├── UnlinkMKV-GUI
├── .idea
│ └── .idea.UnlinkMKV-GUI
│ │ ├── .idea
│ │ ├── .name
│ │ ├── vcs.xml
│ │ └── modules.xml
│ │ └── riderModule.iml
├── UnlinkMKV-GUI
│ ├── merge
│ │ ├── MergeResult.cs
│ │ ├── info
│ │ │ ├── MkvNixException.cs
│ │ │ ├── IMKVInfoLoaderStrategy.cs
│ │ │ └── MkvToolNixMkvInfoLoaderStrategy.cs
│ │ ├── MergeOptions.cs
│ │ ├── MergePart.cs
│ │ ├── extract
│ │ │ ├── AttachmentExtractor.cs
│ │ │ └── BaseMatroskaExtractor.cs
│ │ ├── TimeCodeUtil.cs
│ │ ├── ChapterSelector.cs
│ │ ├── SegmentUtility.cs
│ │ ├── SegmentTimecodeSelector.cs
│ │ └── MergeJob.cs
│ ├── data
│ │ ├── IMkvInfoSummaryMapper.cs
│ │ ├── Chapter.cs
│ │ ├── MkvAttachment.cs
│ │ ├── MkvSegmentUid.cs
│ │ ├── MkvEdition.cs
│ │ ├── ChapterAtom.cs
│ │ ├── MKVInfo.cs
│ │ └── xml
│ │ │ └── XmlMkvInfoSummaryMapper.cs
│ ├── IMkvJobMerger.cs
│ ├── PathUtil.cs
│ ├── Program.cs
│ ├── Properties
│ │ ├── Settings.settings
│ │ ├── AssemblyInfo.cs
│ │ ├── Settings.Designer.cs
│ │ ├── Resources.Designer.cs
│ │ └── Resources.resx
│ ├── ValidationStatusControl.cs
│ ├── Valdiators
│ │ ├── IValidatorTask.cs
│ │ └── Validators.cs
│ ├── App.config
│ ├── ui
│ │ └── TextBoxControlWriter.cs
│ ├── FormValidator.cs
│ ├── PathUtility.cs
│ ├── ValidationStatusControl.Designer.cs
│ ├── legacy
│ │ └── PerlJob.cs
│ ├── FormValidator.Designer.cs
│ ├── FormValidator.resx
│ ├── FormApplication.resx
│ ├── ValidationStatusControl.resx
│ ├── UnlinkMKV-GUI.csproj
│ ├── FormApplication.cs
│ ├── FormApplication.Designer.cs
│ └── winport.pl
├── UnlinkMKV-GUI.Tests
│ ├── packages.config
│ ├── UnlinkMKV-GUI.Tests.csproj
│ └── data
│ │ └── XmlMkvInfoSummaryMapperTest.cs
└── UnlinkMKV-GUI.sln
├── dist
├── UnlinkMKV-GUI.exe
├── UnlinkMKV-GUI.exe.mdb
├── unlinkmkv.ini
└── winport.pl
├── LICENSE
├── .gitattributes
├── .gitignore
└── README.md
/UnlinkMKV-GUI/.idea/.idea.UnlinkMKV-GUI/.idea/.name:
--------------------------------------------------------------------------------
1 | UnlinkMKV-GUI
--------------------------------------------------------------------------------
/dist/UnlinkMKV-GUI.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hilts-vaughan/UnlinkMKV-GUI/HEAD/dist/UnlinkMKV-GUI.exe
--------------------------------------------------------------------------------
/dist/UnlinkMKV-GUI.exe.mdb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hilts-vaughan/UnlinkMKV-GUI/HEAD/dist/UnlinkMKV-GUI.exe.mdb
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/merge/MergeResult.cs:
--------------------------------------------------------------------------------
1 | namespace UnlinkMKV_GUI.merge
2 | {
3 | public enum MergeResult
4 | {
5 | OK,
6 | Error,
7 | NothingToMerge,
8 | }
9 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/.idea/.idea.UnlinkMKV-GUI/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/data/IMkvInfoSummaryMapper.cs:
--------------------------------------------------------------------------------
1 | using System.Xml.Linq;
2 |
3 | namespace UnlinkMKV_GUI.data
4 | {
5 | public interface IMkvInfoSummaryMapper
6 | {
7 | XDocument DecodeStringIntoDocument(string stringSource);
8 | }
9 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/merge/info/MkvNixException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace UnlinkMKV_GUI.merge.info
4 | {
5 | public class MkvNixException : Exception
6 | {
7 | public MkvNixException(string message) : base(message)
8 | {
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/dist/unlinkmkv.ini:
--------------------------------------------------------------------------------
1 | out_dir = /home/touma/Desktop
2 | tmpdir = /tmp/UnlinkMKV
3 | ffmpeg = /usr/bin/ffmpeg
4 | mkvext = /usr/bin/mkvextract
5 | mkvinfo = /usr/bin/mkvinfo
6 | mkvmerge = /usr/bin/mkvmerge
7 | fixaudio = 0
8 | fixvideo = 0
9 | fixsubtitles = 1
10 | ignoredefaultflag = 0
11 | chapters = 1
12 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/merge/info/IMKVInfoLoaderStrategy.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Cryptography.X509Certificates;
2 | using UnlinkMKV_GUI.data;
3 |
4 | namespace UnlinkMKV_GUI.merge.info
5 | {
6 | public interface IMkvInfoLoaderStrategy
7 | {
8 | MkvInfo FetchMkvInfo(string file);
9 | }
10 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/IMkvJobMerger.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Threading.Tasks;
3 | using UnlinkMKV_GUI.merge;
4 |
5 | namespace UnlinkMKV_GUI
6 | {
7 | public interface IMkvJobMerger
8 | {
9 | Task ExecuteJob(TextWriter logger, string source, string dest);
10 | }
11 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/.idea/.idea.UnlinkMKV-GUI/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/merge/MergeOptions.cs:
--------------------------------------------------------------------------------
1 | namespace UnlinkMKV_GUI.merge
2 | {
3 | public class MergeOptions
4 | {
5 | public string TemporaryDirectory { get; set; }
6 | public bool ReEncode { get; set; }
7 | public bool IgnoreDefaultFlag { get; set; }
8 | public bool MaintainChapters { get; set; }
9 | }
10 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/merge/MergePart.cs:
--------------------------------------------------------------------------------
1 | using UnlinkMKV_GUI.data;
2 |
3 | namespace UnlinkMKV_GUI.merge
4 | {
5 | public class MergePart
6 | {
7 | public MergePart(MkvInfo info, string filename)
8 | {
9 | Info = info;
10 | Filename = filename;
11 | }
12 |
13 | public MkvInfo Info { get; }
14 | public string Filename { get; }
15 | }
16 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/PathUtil.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace UnlinkMKV_GUI
4 | {
5 | public static class PathUtil
6 | {
7 | public static string GetTemporaryDirectory()
8 | {
9 | string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
10 | Directory.CreateDirectory(tempDirectory);
11 | return tempDirectory;
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Windows.Forms;
4 | using UnlinkMKV_GUI;
5 | using UnlinkMKV_GUI.merge;
6 |
7 | public class Program
8 | {
9 | [STAThread]
10 | public static void Main(String[] args)
11 | {
12 | Application.EnableVisualStyles();
13 | Application.SetCompatibleTextRenderingDefault(false);
14 | Application.Run(new FormApplication());
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/data/Chapter.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Xml.Linq;
3 |
4 | namespace UnlinkMKV_GUI.data
5 | {
6 | public class Chapter
7 | {
8 | public Chapter(XContainer node)
9 | {
10 | Editions = new List();
11 | var editions = node.Elements("EditionEntry");
12 | foreach (var edition in editions)
13 | {
14 | Editions.Add(new MkvEdition(edition));
15 | }
16 | }
17 |
18 | public List Editions { get; }
19 | }
20 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI.Tests/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/data/MkvAttachment.cs:
--------------------------------------------------------------------------------
1 | using System.Xml.Linq;
2 |
3 | namespace UnlinkMKV_GUI.data
4 | {
5 | public class MkvAttachment
6 | {
7 | public MkvAttachment(XElement attachmentNode)
8 | {
9 | Filename = attachmentNode.Element("FileName")?.Value;
10 | MimeType = attachmentNode.Element("MimeType")?.Value;
11 | FileUid = attachmentNode.Element("FileUID")?.Value;
12 | FileDataSize = int.Parse((attachmentNode.Element("FileDataSize")?.Value));
13 | }
14 |
15 | public string Filename { get; }
16 | public string MimeType { get; }
17 | public string FileUid { get; }
18 | public int FileDataSize { get; }
19 | }
20 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/data/MkvSegmentUid.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace UnlinkMKV_GUI.data
4 | {
5 | public class MkvSegmentUid
6 | {
7 | private string _hexString;
8 |
9 | public MkvSegmentUid(string hexString)
10 | {
11 | this._hexString = hexString.Substring(hexString.IndexOf("0x", StringComparison.Ordinal));
12 | }
13 |
14 | public bool IsSame(MkvSegmentUid segment)
15 | {
16 | if (segment == null)
17 | {
18 | return false;
19 | }
20 | return segment._hexString == this._hexString;
21 | }
22 |
23 | public override string ToString()
24 | {
25 | return this._hexString;
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/ValidationStatusControl.cs:
--------------------------------------------------------------------------------
1 | using System.Drawing;
2 | using System.Data;
3 | using System.Windows.Forms;
4 |
5 | namespace UnlinkMKV_GUI
6 | {
7 | public partial class ValidationStatusControl : UserControl
8 | {
9 | public ValidationStatusControl()
10 | {
11 | InitializeComponent();
12 | }
13 |
14 | public ValidationStatusControl(string message)
15 | {
16 | InitializeComponent();
17 | labelStatus.Text = message;
18 | }
19 |
20 | public void SetStatus(bool status)
21 | {
22 | if (status)
23 | labelStatus.ForeColor = Color.LimeGreen;
24 | else
25 | labelStatus.ForeColor = Color.Red;
26 | }
27 |
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/.idea/.idea.UnlinkMKV-GUI/riderModule.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/merge/extract/AttachmentExtractor.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using UnlinkMKV_GUI.data;
3 |
4 | namespace UnlinkMKV_GUI.merge.extract
5 | {
6 | public class AttachmentExtractor: BaseMatroskaExtractor
7 | {
8 | public override void PerformExtraction(MkvInfo info, string file)
9 | {
10 | // Swap to attachments directory
11 | CreateAndChangeTo("attachments");
12 |
13 | var segment = new MergePart(info, file);
14 |
15 | var attachmentCount = segment.Info.Attachments.Count;
16 | var joinedCounts = "";
17 | for (var i = 0; i < attachmentCount; i++)
18 | {
19 | joinedCounts += $"{i + 1} ";
20 | }
21 | var proc = Process.Start("mkvextract", $"attachments \"{segment.Filename}\" {joinedCounts}");
22 | proc.WaitForExit();
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/Valdiators/IValidatorTask.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 |
3 | namespace UnlinkMKV_GUI.Valdiators
4 | {
5 | public interface IValidatorTask
6 | {
7 |
8 | ///
9 | /// Returns a status text string representing this task
10 | ///
11 | ///
12 | string GetStatusText();
13 |
14 | ///
15 | /// Performs a check to see if the requirement is possible
16 | ///
17 | ///
18 | bool IsRequirementMet();
19 |
20 | ///
21 | /// Attempts to fix a broken requirement test when possible.
22 | ///
23 | /// This will return true when it succeeds and false otherwise.
24 | bool AttemptFixRequirement();
25 | }
26 |
27 |
28 | // For simplicity sake, we'll just put all the validators in their own file
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/merge/TimeCodeUtil.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace UnlinkMKV_GUI.merge
4 | {
5 | public static class TimeCodeUtil
6 | {
7 | public static TimeSpan TimeCodeToTimespan(string timeCode)
8 | {
9 | var splits = timeCode.Split(':');
10 | var hour = splits[0];
11 | var minute = splits[1];
12 | var second = splits[2].Split('.')[0];
13 |
14 | return new TimeSpan(0, int.Parse(hour), int.Parse(minute), int.Parse(second));
15 | }
16 |
17 | public static string TimespanToTimeCode(TimeSpan timespan)
18 | {
19 | var hours = timespan.Hours.ToString("D2");
20 | var minutes = timespan.Minutes.ToString("D2");
21 | var seconds = timespan.Seconds.ToString("D2");
22 | var milliseconds = timespan.Milliseconds.ToString(("D9"));
23 |
24 | return $"{hours}:{minutes}:{seconds}:{milliseconds}";
25 | }
26 |
27 | }
28 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/merge/ChapterSelector.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using UnlinkMKV_GUI.data;
4 |
5 | namespace UnlinkMKV_GUI.merge
6 | {
7 | ///
8 | /// Selects chapters that are required for processing based on some rules.
9 | /// As time goes on, I am sure some releases will have some hacks that are required to be applied to them that
10 | /// we can probably have this selector apply. These are problably distributed in the form of decorators or something
11 | /// that can be compiled and ran or just simply C# files with logic that can be additionally tagged and downloaded
12 | /// from the web.
13 | ///
14 | public class ChapterSelector
15 | {
16 | IList GetRelevantChapters(IList chapters)
17 | {
18 | // For now, just strip off undefined chapter languages
19 | return chapters.Where(x => x.ChapterLanguage != "und").ToList();
20 | }
21 |
22 | }
23 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/data/MkvEdition.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Xml.Linq;
4 |
5 | namespace UnlinkMKV_GUI.data
6 | {
7 | public class MkvEdition
8 | {
9 | public MkvEdition(XContainer editionNode)
10 | {
11 | Chapters = new List();
12 | var chapterNodes = editionNode.Elements().Where(x => x.Name == "ChapterAtom");
13 | foreach (var chapterNode in chapterNodes)
14 | Chapters.Add(new ChapterAtom(chapterNode));
15 |
16 | // We just assume in good faaith here that this is all valid
17 | var isDefaultBitFlag = int.Parse(editionNode.Element("EditionFlagDefault")?.Value);
18 | if (isDefaultBitFlag == 1)
19 | {
20 | IsDefault = true;
21 | }
22 | }
23 |
24 | public bool IsDefault { get; }
25 | public int EditionUid { get;}
26 |
27 | public List Chapters { get; private set; }
28 | }
29 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Vaughan Hilts
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 |
23 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/merge/extract/BaseMatroskaExtractor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using UnlinkMKV_GUI.data;
4 |
5 | namespace UnlinkMKV_GUI.merge.extract
6 | {
7 | public abstract class BaseMatroskaExtractor : IDisposable
8 | {
9 | private string _oldPath;
10 |
11 | ///
12 | /// Performs the request extract of elements on the given file.
13 | ///
14 | public abstract void PerformExtraction(MkvInfo segment, string file);
15 |
16 |
17 | protected string CreateAndChangeTo(string request)
18 | {
19 | // OK, change directory to make sure things are outputted there
20 | _oldPath = Environment.CurrentDirectory;
21 |
22 | var newPath = Path.Combine(_oldPath, request);
23 | Directory.CreateDirectory(newPath);
24 | Environment.CurrentDirectory = newPath;
25 |
26 | return this._oldPath;
27 | }
28 |
29 | public void Dispose()
30 | {
31 | // Make sure we restore once we're done with this
32 | Environment.CurrentDirectory = _oldPath;
33 | }
34 |
35 | }
36 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/merge/SegmentUtility.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using UnlinkMKV_GUI.data;
4 | using UnlinkMKV_GUI.merge.info;
5 |
6 | namespace UnlinkMKV_GUI.merge
7 | {
8 | public static class SegmentUtility
9 | {
10 | public static List GetMergePartsForFilename(string filename, MkvInfo baseInfo)
11 | {
12 | var result = new List();
13 |
14 | var candidates = Directory.GetFiles(Path.GetDirectoryName(filename), "*.mkv");
15 | var loader = new MkvToolNixMkvInfoLoaderStrategy();
16 | foreach (var candidate in candidates)
17 | {
18 | if(candidate == filename) continue;
19 |
20 | var info = loader.FetchMkvInfo(candidate);
21 | var isSafe = false;
22 | baseInfo.Chapters.Editions[0].Chapters.ForEach((x => isSafe |= info.SegmentUid.IsSame(x.ReferencedSegmentUid)));
23 | if (isSafe)
24 | {
25 | result.Add(new MergePart(info, candidate));
26 | }
27 | }
28 |
29 | return result;
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/ui/TextBoxControlWriter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 | using System.Text.RegularExpressions;
5 | using System.Windows.Forms;
6 |
7 | namespace UnlinkMKV_GUI.ui
8 | {
9 | public class TextBoxControlWriter : TextWriter
10 | {
11 | private readonly Control _textbox;
12 | private readonly Form _owner;
13 |
14 | public TextBoxControlWriter(Form owner, Control textbox)
15 | {
16 | this._textbox = textbox;
17 | this._owner = owner;
18 | }
19 | public override void Write(char value)
20 | {
21 | ExecuteSecure(() => _textbox.Text += value);
22 | }
23 |
24 | public override void Write(string value)
25 | {
26 | ExecuteSecure(() => _textbox.Text += CleanEscape(value));
27 | }
28 |
29 | public override Encoding Encoding => Encoding.ASCII;
30 |
31 | private string CleanEscape(string inputString) {
32 | return Regex.Replace(inputString, @"\e\[(\d+;)*(\d+)?[ABCDHJKfmsu]", "");
33 | }
34 |
35 | private void ExecuteSecure(Action a) {
36 | if (_owner.InvokeRequired)
37 | _owner.BeginInvoke(a);
38 | else
39 | a();
40 | }
41 |
42 | }
43 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | // General Information about an assembly is controlled through the following
2 | // set of attributes. Change these attribute values to modify the information
3 | // associated with an assembly.
4 | using System.Reflection;
5 | using System.Runtime.InteropServices;
6 |
7 | [assembly: AssemblyTitle("UnlinkMKV-GUI")]
8 | [assembly: AssemblyDescription("")]
9 | [assembly: AssemblyConfiguration("")]
10 | [assembly: AssemblyCompany("")]
11 | [assembly: AssemblyProduct("UnlinkMKV-GUI")]
12 | [assembly: AssemblyCopyright("Copyright © 2015")]
13 | [assembly: AssemblyTrademark("")]
14 | [assembly: AssemblyCulture("")]
15 |
16 | // Setting ComVisible to false makes the types in this assembly not visible
17 | // to COM components. If you need to access a type in this assembly from
18 | // COM, set the ComVisible attribute to true on that type.
19 | [assembly: ComVisible(false)]
20 |
21 | // The following GUID is for the ID of the typelib if this project is exposed to COM
22 | [assembly: Guid("fb9557b2-7f20-4cf3-bfd7-3fad407fc275")]
23 |
24 | // Version information for an assembly consists of the following four values:
25 | //
26 | // Major Version
27 | // Minor Version
28 | // Build Number
29 | // Revision
30 | //
31 | // You can specify all the values or you can default the Build and Revision Numbers
32 | // by using the '*' as shown below:
33 | // [assembly: AssemblyVersion("1.0.*")]
34 | [assembly: AssemblyVersion("1.0.0.0")]
35 | [assembly: AssemblyFileVersion("1.0.0.0")]
36 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 2013
4 | VisualStudioVersion = 12.0.30324.0
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnlinkMKV-GUI", "UnlinkMKV-GUI\UnlinkMKV-GUI.csproj", "{31A91B00-390D-43D6-9655-633503D1821E}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnlinkMKV-GUI.Tests", "UnlinkMKV-GUI.Tests\UnlinkMKV-GUI.Tests.csproj", "{62D45073-41F8-4079-BCED-7AE6F51F43EC}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {31A91B00-390D-43D6-9655-633503D1821E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {31A91B00-390D-43D6-9655-633503D1821E}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {31A91B00-390D-43D6-9655-633503D1821E}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {31A91B00-390D-43D6-9655-633503D1821E}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {62D45073-41F8-4079-BCED-7AE6F51F43EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {62D45073-41F8-4079-BCED-7AE6F51F43EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {62D45073-41F8-4079-BCED-7AE6F51F43EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {62D45073-41F8-4079-BCED-7AE6F51F43EC}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | EndGlobal
29 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/merge/info/MkvToolNixMkvInfoLoaderStrategy.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Xml.Linq;
3 | using System.Xml.XPath;
4 | using UnlinkMKV_GUI.data;
5 | using UnlinkMKV_GUI.data.xml;
6 |
7 | namespace UnlinkMKV_GUI.merge.info
8 | {
9 | public class MkvToolNixMkvInfoLoaderStrategy : IMkvInfoLoaderStrategy
10 | {
11 | public MkvInfo FetchMkvInfo(string file)
12 | {
13 | var proc = new Process {StartInfo = new ProcessStartInfo("mkvinfo", $"\"{file}\"") {RedirectStandardOutput = true, UseShellExecute = false}};
14 |
15 | if (proc == null)
16 | {
17 | throw new MkvNixException("Failed to find / start mkvinfo");
18 | }
19 |
20 | proc.Start();
21 | var data = proc.StandardOutput.ReadToEnd();
22 | proc.WaitForExit();
23 |
24 | var mapper = new XmlMkvInfoSummaryMapper();
25 | var doc = mapper.DecodeStringIntoDocument(data);
26 |
27 | // Append some extract MediaInfo data that we might need... :(
28 | var mediaInfoProc = new Process
29 | {
30 | StartInfo =
31 | new ProcessStartInfo("mediainfo", $"--Inform=\"Video;%Duration/String3%\" \"{file}\"") {RedirectStandardOutput = true, UseShellExecute = false}
32 | };
33 |
34 | mediaInfoProc.Start();
35 | var mediaInfoStr = mediaInfoProc.StandardOutput.ReadToEnd();
36 | mediaInfoProc.WaitForExit();
37 |
38 | // mediainfo > file > duration... weird format
39 | var mkvInfo = new MkvInfo(doc) {Duration = TimeCodeUtil.TimeCodeToTimespan(mediaInfoStr)};
40 | return mkvInfo;
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/data/ChapterAtom.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Xml.Linq;
3 |
4 | namespace UnlinkMKV_GUI.data
5 | {
6 | public class ChapterAtom : IEquatable
7 | {
8 | public ChapterAtom(XContainer chapterNode)
9 | {
10 | ChapterTimecodeStart = chapterNode.Element("ChapterTimeStart")?.Value;
11 | ChapterTimecodeEnd = chapterNode.Element("ChapterTimeEnd")?.Value;
12 | ChapterName = chapterNode.Element("ChapterDisplay")?.Element("ChapterString")?.Value;
13 | ChapterLanguage = chapterNode.Element("ChapterDisplay")?.Element("ChapterLanguage")?.Value;
14 |
15 | var segmentNode = chapterNode.Element("ChapterSegmentUID");
16 | if (segmentNode != null)
17 | {
18 | ReferencedSegmentUid = new MkvSegmentUid(segmentNode.Value);
19 | }
20 | }
21 |
22 | public string ChapterTimecodeEnd { get; set; }
23 | public string ChapterTimecodeStart { get; set; }
24 | public string ChapterName { get; }
25 | public string ChapterLanguage { get; }
26 |
27 | ///
28 | /// Represents the segment UID that this chapter might actually be linked to, if it is linked.
29 | ///
30 | public MkvSegmentUid ReferencedSegmentUid { get; private set; }
31 |
32 | ///
33 | /// Returns whether or not this chapter atom is linked.
34 | ///
35 | ///
36 | public bool IsLinked()
37 | {
38 | return ReferencedSegmentUid != null;
39 | }
40 |
41 |
42 | public bool Equals(ChapterAtom other)
43 | {
44 | return other?.ChapterName == this.ChapterName;
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/FormValidator.cs:
--------------------------------------------------------------------------------
1 | using System.Windows.Forms;
2 | using UnlinkMKV_GUI.Valdiators;
3 | using System.Collections.Generic;
4 | using System;
5 |
6 | namespace UnlinkMKV_GUI
7 | {
8 | public partial class FormValidator : Form
9 | {
10 | public FormValidator()
11 | {
12 | InitializeComponent();
13 |
14 | // We're not too concerned with threading manipulations here, so we won't worry about it here
15 | CheckForIllegalCrossThreadCalls = false;
16 |
17 | }
18 |
19 | private void CheckRequirements()
20 | {
21 | var validatorTasks = new List();
22 | validatorTasks.Add(new IsAdministratorValidatorTask());
23 | validatorTasks.Add(new MkvToolNixValidatorTask());;
24 | validatorTasks.Add(new PerlExistsValidatorTask());
25 |
26 | foreach (var task in validatorTasks)
27 | {
28 | var statusControl = new ValidationStatusControl(task.GetStatusText());
29 | flowLayoutPanel1.Controls.Add(statusControl);
30 |
31 | bool requirementMet = task.IsRequirementMet();
32 |
33 | if (!requirementMet)
34 | {
35 | bool wasFixed = task.AttemptFixRequirement();
36 |
37 | if (!wasFixed)
38 | {
39 | Application.Exit();
40 | }
41 | }
42 |
43 | // Otherwise, okay move onto the next
44 | statusControl.SetStatus(true);
45 |
46 | Application.DoEvents();
47 | }
48 |
49 | Close();
50 |
51 | }
52 |
53 | private void FormValidator_Load(object sender, EventArgs e)
54 | {
55 |
56 | }
57 |
58 | private void FormValidator_Shown(object sender, EventArgs e)
59 | {
60 | CheckRequirements();
61 | }
62 |
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.18449
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace UnlinkMKV_GUI.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "12.0.0.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 |
26 | [global::System.Configuration.UserScopedSettingAttribute()]
27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
28 | [global::System.Configuration.DefaultSettingValueAttribute("")]
29 | public string input {
30 | get {
31 | return ((string)(this["input"]));
32 | }
33 | set {
34 | this["input"] = value;
35 | }
36 | }
37 |
38 | [global::System.Configuration.UserScopedSettingAttribute()]
39 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
40 | [global::System.Configuration.DefaultSettingValueAttribute("")]
41 | public string output {
42 | get {
43 | return ((string)(this["output"]));
44 | }
45 | set {
46 | this["output"] = value;
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/PathUtility.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace UnlinkMKV_GUI
5 | {
6 | public static class PathUtility
7 | {
8 | public static string ExceptionalPath = "";
9 |
10 | // Thanks!
11 | // http://csharptest.net/526/how-to-search-the-environments-path-for-an-exe-or-dll/
12 |
13 | ///
14 | /// Expands environment variables and, if unqualified, locates the exe in the working directory
15 | /// or the evironment's path.
16 | ///
17 | /// The name of the executable file
18 | /// The fully-qualified path to the file
19 | /// Raised when the exe was not found
20 | public static string FindExePath(string exe)
21 | {
22 |
23 | // Consulo vs Rider
24 | // Which is better in the end? Well, I'm not really sure since it really will boil downt to whatever
25 | // you think is the better tool.
26 |
27 | // If we're not on Windows, remove ".EXE"
28 | var p = (int)Environment.OSVersion.Platform;
29 | if ((p == 4) || (p == 6) || (p == 128))
30 | {
31 | // Running on Unix, strip EXE
32 | exe = exe.Replace(".exe", "");
33 | }
34 |
35 | exe = Environment.ExpandEnvironmentVariables(exe);
36 | if (File.Exists(exe)) return Path.GetFullPath(exe);
37 | if (Path.GetDirectoryName(exe) != String.Empty)
38 | throw new FileNotFoundException(new FileNotFoundException().Message, exe);
39 |
40 | foreach (var test in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator))
41 | {
42 | var path = test.Trim();
43 | path = Path.Combine(path, exe);
44 | if (!string.IsNullOrEmpty(path) && File.Exists(path))
45 | return Path.GetFullPath(path);
46 | }
47 | throw new FileNotFoundException(new FileNotFoundException().Message, exe);
48 | }
49 |
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/data/MKVInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Xml.Linq;
5 | using System.Xml.XPath;
6 |
7 | namespace UnlinkMKV_GUI.data
8 | {
9 | ///
10 | /// Represents some basic data around a MKV file that has been processed.
11 | ///
12 | public class MkvInfo
13 | {
14 | public MkvInfo(XDocument source)
15 | {
16 | // Build up our data model to work with here... and then create a CLI utility that works everywhere. :)
17 | // Probably can just make it take one file and then xargs them together in a chain
18 |
19 | var root = source.XPathSelectElement("MKVInfo/SegmentSize/SegmentInformation");
20 | var outsideRoot = source.XPathSelectElement("MKVInfo/SegmentSize");
21 |
22 | if (root != null)
23 | {
24 | Writer = root.Element("WritingApplication")?.Value;
25 |
26 | // Create attachments
27 | Attachments = new List();
28 | var attachmentNodes = outsideRoot.Element("Attachments").Elements();
29 | foreach (var attachmentNode in attachmentNodes)
30 | {
31 | Attachments.Add(new MkvAttachment(attachmentNode));
32 | }
33 |
34 | // Create chapters
35 | var chapters = outsideRoot.Element("Chapters");
36 | if (chapters != null)
37 | {
38 | Chapters = new Chapter(chapters);
39 | }
40 |
41 | SegmentUid = new MkvSegmentUid(source.XPathSelectElement("MKVInfo/SegmentSize/SegmentInformation/SegmentUID").Value);
42 | }
43 | else
44 | {
45 | throw new Exception("Failed to find the node that was needed.");
46 | }
47 |
48 | }
49 |
50 | public DateTime DateAuthored { get; }
51 | public string Writer { get; }
52 | public List Attachments { get; }
53 | public MkvSegmentUid SegmentUid { get; }
54 | public Chapter Chapters { get; }
55 | public TimeSpan Duration { get; set; }
56 |
57 | public bool IsFileLinked()
58 | {
59 | return Chapters.Editions.Any(x => x.Chapters.Any(y => y.IsLinked()));
60 | }
61 |
62 | }
63 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/ValidationStatusControl.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace UnlinkMKV_GUI
2 | {
3 | partial class ValidationStatusControl
4 | {
5 | ///
6 | /// Required designer variable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Clean up any resources being used.
12 | ///
13 | /// true if managed resources should be disposed; otherwise, false.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Component Designer generated code
24 |
25 | ///
26 | /// Required method for Designer support - do not modify
27 | /// the contents of this method with the code editor.
28 | ///
29 | private void InitializeComponent()
30 | {
31 | this.labelStatus = new System.Windows.Forms.Label();
32 | this.SuspendLayout();
33 | //
34 | // labelStatus
35 | //
36 | this.labelStatus.AutoSize = true;
37 | this.labelStatus.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
38 | this.labelStatus.Location = new System.Drawing.Point(13, 9);
39 | this.labelStatus.Name = "labelStatus";
40 | this.labelStatus.Size = new System.Drawing.Size(217, 20);
41 | this.labelStatus.TabIndex = 0;
42 | this.labelStatus.Text = "Checking for MKVMerge...";
43 | //
44 | // ValidationStatusControl
45 | //
46 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
47 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
48 | this.Controls.Add(this.labelStatus);
49 | this.Name = "ValidationStatusControl";
50 | this.Size = new System.Drawing.Size(364, 40);
51 | this.ResumeLayout(false);
52 | this.PerformLayout();
53 |
54 | }
55 |
56 | #endregion
57 |
58 | private System.Windows.Forms.Label labelStatus;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.18449
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace UnlinkMKV_GUI.Properties
12 | {
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources
26 | {
27 |
28 | private static global::System.Resources.ResourceManager resourceMan;
29 |
30 | private static global::System.Globalization.CultureInfo resourceCulture;
31 |
32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
33 | internal Resources()
34 | {
35 | }
36 |
37 | ///
38 | /// Returns the cached ResourceManager instance used by this class.
39 | ///
40 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
41 | internal static global::System.Resources.ResourceManager ResourceManager
42 | {
43 | get
44 | {
45 | if ((resourceMan == null))
46 | {
47 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("UnlinkMKV_GUI.Properties.Resources", typeof(Resources).Assembly);
48 | resourceMan = temp;
49 | }
50 | return resourceMan;
51 | }
52 | }
53 |
54 | ///
55 | /// Overrides the current thread's CurrentUICulture property for all
56 | /// resource lookups using this strongly typed resource class.
57 | ///
58 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
59 | internal static global::System.Globalization.CultureInfo Culture
60 | {
61 | get
62 | {
63 | return resourceCulture;
64 | }
65 | set
66 | {
67 | resourceCulture = value;
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/legacy/PerlJob.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Threading.Tasks;
5 | using System.Windows.Forms;
6 | using UnlinkMKV_GUI.merge;
7 |
8 | namespace UnlinkMKV_GUI.legacy
9 | {
10 | public class PerlJob : IMkvJobMerger
11 | {
12 | private readonly string _options;
13 |
14 | public PerlJob(string options)
15 | {
16 | _options = options;
17 | }
18 |
19 | public async Task ExecuteJob(TextWriter log, string source, string destination)
20 | {
21 | var configPath = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "unlinkmkv.ini");
22 | var config = new Config(configPath);
23 |
24 | var merge = PathUtility.FindExePath("mkvmerge.exe");
25 | var info = PathUtility.FindExePath("mkvinfo.exe");
26 | var extract = PathUtility.FindExePath("mkvextract.exe");
27 | var ffmpeg = "";
28 |
29 | try
30 | {
31 | ffmpeg = PathUtility.FindExePath("ffmpeg.exe");
32 | }
33 | catch(Exception e)
34 | {
35 | // Do nothing
36 | }
37 |
38 | config.OutputPath = destination;
39 | config.SetRequiredPaths(ffmpeg, extract, info, merge);
40 | config.Persist(configPath);
41 |
42 | var perlParams = _options;
43 | var perlPath = PathUtility.FindExePath("perl.exe");
44 | var outDirectory = "--outdir \"" + destination+ "\"";
45 | var perlScript = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "winport.pl");
46 |
47 | var file = source;
48 |
49 |
50 | // Generate a Perl job
51 | var quotedFile = "\"" + file + "\"";
52 | var argument = "\"" + perlScript + "\" " + perlParams + " " + outDirectory + " " + quotedFile;
53 |
54 | // PathUtility.ExceptionalPath
55 | var startInfo = new ProcessStartInfo
56 | {
57 | FileName = perlPath,
58 | Arguments = argument,
59 | UseShellExecute = false,
60 | RedirectStandardOutput = true,
61 | RedirectStandardError = true,
62 | CreateNoWindow = true
63 | };
64 |
65 | string x = perlPath + " " + argument;
66 | log.WriteLine(x);
67 |
68 | // Add the path if required to the Perl executing path
69 | if (!string.IsNullOrEmpty(PathUtility.ExceptionalPath))
70 | {
71 | startInfo.EnvironmentVariables["PATH"] = PathUtility.ExceptionalPath;
72 | }
73 |
74 | var perlJob = new Process()
75 | {
76 | StartInfo = startInfo
77 | };
78 |
79 |
80 | perlJob.ErrorDataReceived += (s, e) => log.WriteLine(e.Data);
81 |
82 | perlJob.Start();
83 |
84 | while (!perlJob.StandardOutput.EndOfStream) {
85 | var line = perlJob.StandardOutput.ReadLine();
86 | log.WriteLine(line);
87 | }
88 |
89 | perlJob.WaitForExit();
90 |
91 | return MergeResult.OK;
92 | }
93 | }
94 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/FormValidator.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace UnlinkMKV_GUI
2 | {
3 | partial class FormValidator
4 | {
5 | ///
6 | /// Required designer variable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Clean up any resources being used.
12 | ///
13 | /// true if managed resources should be disposed; otherwise, false.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Windows Form Designer generated code
24 |
25 | ///
26 | /// Required method for Designer support - do not modify
27 | /// the contents of this method with the code editor.
28 | ///
29 | private void InitializeComponent()
30 | {
31 | this.label1 = new System.Windows.Forms.Label();
32 | this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel();
33 | this.SuspendLayout();
34 | //
35 | // label1
36 | //
37 | this.label1.AutoSize = true;
38 | this.label1.Dock = System.Windows.Forms.DockStyle.Top;
39 | this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
40 | this.label1.Location = new System.Drawing.Point(0, 0);
41 | this.label1.Name = "label1";
42 | this.label1.Size = new System.Drawing.Size(267, 16);
43 | this.label1.TabIndex = 0;
44 | this.label1.Text = "Checking if all requirements are met...";
45 | //
46 | // flowLayoutPanel1
47 | //
48 | this.flowLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Bottom;
49 | this.flowLayoutPanel1.Location = new System.Drawing.Point(0, 19);
50 | this.flowLayoutPanel1.Name = "flowLayoutPanel1";
51 | this.flowLayoutPanel1.Size = new System.Drawing.Size(688, 207);
52 | this.flowLayoutPanel1.TabIndex = 1;
53 | //
54 | // FormValidator
55 | //
56 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
57 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
58 | this.ClientSize = new System.Drawing.Size(688, 226);
59 | this.ControlBox = false;
60 | this.Controls.Add(this.flowLayoutPanel1);
61 | this.Controls.Add(this.label1);
62 | this.Name = "FormValidator";
63 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
64 | this.Text = "Validating requirements...";
65 | this.Load += new System.EventHandler(this.FormValidator_Load);
66 | this.Shown += new System.EventHandler(this.FormValidator_Shown);
67 | this.ResumeLayout(false);
68 | this.PerformLayout();
69 |
70 | }
71 |
72 | #endregion
73 |
74 | private System.Windows.Forms.Label label1;
75 | private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel1;
76 | }
77 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI.Tests/UnlinkMKV-GUI.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {62D45073-41F8-4079-BCED-7AE6F51F43EC}
8 | {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
9 | Library
10 | Properties
11 | UnlinkMKV_GUI.Tests
12 | UnlinkMKV_GUI.Tests
13 | v4.5
14 | 512
15 |
16 |
17 | AnyCPU
18 | true
19 | full
20 | false
21 | bin\Debug\
22 | DEBUG;TRACE
23 | prompt
24 | 4
25 |
26 |
27 | AnyCPU
28 | pdbonly
29 | true
30 | bin\Release\
31 | TRACE
32 | prompt
33 | 4
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll
43 |
44 |
45 | ..\packages\xunit.assert.2.2.0-beta4-build3444\lib\netstandard1.0\xunit.assert.dll
46 |
47 |
48 | ..\packages\xunit.extensibility.core.2.2.0-beta4-build3444\lib\net45\xunit.core.dll
49 |
50 |
51 | ..\packages\xunit.extensibility.execution.2.2.0-beta4-build3444\lib\net45\xunit.execution.desktop.dll
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | {31A91B00-390D-43D6-9655-633503D1821E}
60 | UnlinkMKV-GUI
61 |
62 |
63 |
64 |
71 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project specific ignores
2 | UnlinkMKV-GUI/out/**
3 | UnlinkMKV-GUI/.consulo/**
4 |
5 | ## Ignore Visual Studio temporary files, build results, and
6 | ## files generated by popular Visual Studio add-ons.
7 |
8 | # User-specific files
9 | *.suo
10 | *.user
11 | *.userosscache
12 | *.sln.docstates
13 |
14 | # User-specific files (MonoDevelop/Xamarin Studio)
15 | *.userprefs
16 |
17 | # Build results
18 | [Dd]ebug/
19 | [Dd]ebugPublic/
20 | [Rr]elease/
21 | [Rr]eleases/
22 | x64/
23 | x86/
24 | build/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 |
29 | # Visual Studo 2015 cache/options directory
30 | .vs/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | *_i.c
46 | *_p.c
47 | *_i.h
48 | *.ilk
49 | *.meta
50 | *.obj
51 | *.pch
52 | *.pdb
53 | *.pgc
54 | *.pgd
55 | *.rsp
56 | *.sbr
57 | *.tlb
58 | *.tli
59 | *.tlh
60 | *.tmp
61 | *.tmp_proj
62 | *.log
63 | *.vspscc
64 | *.vssscc
65 | .builds
66 | *.pidb
67 | *.svclog
68 | *.scc
69 |
70 | # Chutzpah Test files
71 | _Chutzpah*
72 |
73 | # Visual C++ cache files
74 | ipch/
75 | *.aps
76 | *.ncb
77 | *.opensdf
78 | *.sdf
79 | *.cachefile
80 |
81 | # Visual Studio profiler
82 | *.psess
83 | *.vsp
84 | *.vspx
85 |
86 | # TFS 2012 Local Workspace
87 | $tf/
88 |
89 | # Guidance Automation Toolkit
90 | *.gpState
91 |
92 | # ReSharper is a .NET coding add-in
93 | _ReSharper*/
94 | *.[Rr]e[Ss]harper
95 | *.DotSettings.user
96 |
97 | # JustCode is a .NET coding addin-in
98 | .JustCode
99 |
100 | # TeamCity is a build add-in
101 | _TeamCity*
102 |
103 | # DotCover is a Code Coverage Tool
104 | *.dotCover
105 |
106 | # NCrunch
107 | _NCrunch_*
108 | .*crunch*.local.xml
109 |
110 | # MightyMoose
111 | *.mm.*
112 | AutoTest.Net/
113 |
114 | # Web workbench (sass)
115 | .sass-cache/
116 |
117 | # Installshield output folder
118 | [Ee]xpress/
119 |
120 | # DocProject is a documentation generator add-in
121 | DocProject/buildhelp/
122 | DocProject/Help/*.HxT
123 | DocProject/Help/*.HxC
124 | DocProject/Help/*.hhc
125 | DocProject/Help/*.hhk
126 | DocProject/Help/*.hhp
127 | DocProject/Help/Html2
128 | DocProject/Help/html
129 |
130 | # Click-Once directory
131 | publish/
132 |
133 | # Publish Web Output
134 | *.[Pp]ublish.xml
135 | *.azurePubxml
136 | # TODO: Comment the next line if you want to checkin your web deploy settings
137 | # but database connection strings (with potential passwords) will be unencrypted
138 | *.pubxml
139 | *.publishproj
140 |
141 | # NuGet Packages
142 | *.nupkg
143 | # The packages folder can be ignored because of Package Restore
144 | **/packages/*
145 | # except build/, which is used as an MSBuild target.
146 | !**/packages/build/
147 | # Uncomment if necessary however generally it will be regenerated when needed
148 | #!**/packages/repositories.config
149 |
150 | # Windows Azure Build Output
151 | csx/
152 | *.build.csdef
153 |
154 | # Windows Store app package directory
155 | AppPackages/
156 |
157 | # Others
158 | *.[Cc]ache
159 | ClientBin/
160 | [Ss]tyle[Cc]op.*
161 | ~$*
162 | *~
163 | *.dbmdl
164 | *.dbproj.schemaview
165 | *.pfx
166 | *.publishsettings
167 | node_modules/
168 | bower_components/
169 |
170 | # RIA/Silverlight projects
171 | Generated_Code/
172 |
173 | # Backup & report files from converting an old project file
174 | # to a newer Visual Studio version. Backup files are not needed,
175 | # because we have git ;-)
176 | _UpgradeReport_Files/
177 | Backup*/
178 | UpgradeLog*.XML
179 | UpgradeLog*.htm
180 |
181 | # SQL Server files
182 | *.mdf
183 | *.ldf
184 |
185 | # Business Intelligence projects
186 | *.rdl.data
187 | *.bim.layout
188 | *.bim_*.settings
189 |
190 | # Microsoft Fakes
191 | FakesAssemblies/
192 |
193 | # Node.js Tools for Visual Studio
194 | .ntvs_analysis.dat
195 |
196 | # Visual Studio 6 build log
197 | *.plg
198 |
199 | # Visual Studio 6 workspace options file
200 | *.opt
201 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/Valdiators/Validators.cs:
--------------------------------------------------------------------------------
1 | using System.Windows.Forms;
2 | using System.IO;
3 | using System;
4 | using System.Security.Principal;
5 |
6 | namespace UnlinkMKV_GUI.Valdiators
7 | {
8 | public class MkvToolNixValidatorTask : IValidatorTask
9 | {
10 | public string GetStatusText()
11 | {
12 | return "Detecting MkvToolNix in path and FFMPEG...";
13 | }
14 |
15 | public bool IsRequirementMet()
16 | {
17 |
18 | // Check for the path's to the tools we required
19 | try
20 | {
21 |
22 | PathUtility.FindExePath("mkvextract.exe");
23 | PathUtility.FindExePath("mkvinfo.exe");
24 | PathUtility.FindExePath("mkvmerge.exe");
25 | PathUtility.FindExePath("mediainfo.exe");
26 | }
27 | catch (FileNotFoundException exception)
28 | {
29 | return false;
30 | }
31 |
32 | return true;
33 | }
34 |
35 | public bool AttemptFixRequirement()
36 | {
37 | int p = (int)Environment.OSVersion.Platform;
38 | if ((p == 4) || (p == 6) || (p == 128))
39 | {
40 | return false;
41 | }
42 |
43 |
44 | // Attempt to find them in the path and adjust the path silently, so there's no errors
45 |
46 |
47 | // It looks like it was installed on C:
48 | foreach (var drive in Directory.GetLogicalDrives())
49 | {
50 | var hasNixInstalled = string.Format(@"{0}Program Files (x86)\MKVToolNix\", drive);
51 |
52 | if (Directory.Exists(hasNixInstalled) && File.Exists(Path.Combine(hasNixInstalled, "mkvinfo.exe")))
53 | {
54 | var modPath = Environment.GetEnvironmentVariable("PATH") + ";" + hasNixInstalled;
55 |
56 | // OK, looks like it was installed in the default directory
57 | Environment.SetEnvironmentVariable("PATH",
58 | modPath);
59 |
60 | PathUtility.ExceptionalPath = modPath;
61 |
62 | return true;
63 | }
64 |
65 |
66 | var hasNixInstalled32 = string.Format(@"{0}Program Files\MKVToolNix\", drive);
67 |
68 |
69 | if (Directory.Exists(hasNixInstalled32) && File.Exists(Path.Combine(hasNixInstalled32, "mkvinfo.exe")))
70 | {
71 | var modPath = Environment.GetEnvironmentVariable("PATH") + ";" + hasNixInstalled32;
72 | // OK, looks like it was installed in the default directory
73 | Environment.SetEnvironmentVariable("PATH",
74 | modPath);
75 |
76 | PathUtility.ExceptionalPath = modPath;
77 |
78 | return true;
79 | }
80 | }
81 |
82 | MessageBox.Show(
83 | "MKVToolNix and/or FFMPEG are missing from your system path. Please refer to the manual provided to install them for your platform.");
84 | return false;
85 | }
86 | }
87 |
88 | public class IsAdministratorValidatorTask : IValidatorTask
89 | {
90 | public string GetStatusText()
91 | {
92 | return "Checking for administrator rights...";
93 | }
94 |
95 | public bool IsRequirementMet()
96 | {
97 |
98 | // If we're not on Windows, remove ".EXE"
99 | int p = (int)Environment.OSVersion.Platform;
100 | if ((p == 4) || (p == 6) || (p == 128))
101 | {
102 | return true;
103 | }
104 |
105 |
106 | return (new WindowsPrincipal(WindowsIdentity.GetCurrent()))
107 | .IsInRole(WindowsBuiltInRole.Administrator);
108 | }
109 |
110 | public bool AttemptFixRequirement()
111 | {
112 | MessageBox.Show("This tool requires administrator rights. Please rerun as an administrator.");
113 | return false;
114 | }
115 | }
116 |
117 |
118 | public class PerlExistsValidatorTask : IValidatorTask
119 | {
120 | public string GetStatusText()
121 | {
122 | return "Checking for Strawberry Perl...";
123 | }
124 |
125 | public bool IsRequirementMet()
126 | {
127 | // Check for the path's to the tools we required
128 | try
129 | {
130 |
131 | PathUtility.FindExePath("perl.exe");
132 | }
133 | catch (FileNotFoundException exception)
134 | {
135 | return false;
136 | }
137 |
138 | return true;
139 | }
140 |
141 | public bool AttemptFixRequirement()
142 | {
143 | MessageBox.Show(
144 | "Strawberry Perl was not found. Please include a copy inside your working directory with installed modules. You should have got a copy with this release.");
145 | return false;
146 | }
147 | }
148 |
149 |
150 | }
151 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/data/xml/XmlMkvInfoSummaryMapper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text.RegularExpressions;
5 | using System.Xml;
6 | using System.Xml.Linq;
7 | using static System.Char;
8 |
9 | namespace UnlinkMKV_GUI.data.xml
10 | {
11 | public class XmlMkvInfoSummaryMapper : IMkvInfoSummaryMapper
12 | {
13 | public XDocument DecodeStringIntoDocument(string sourceString)
14 | {
15 | var resultDocument = new XDocument();
16 | var root = new XElement("MKVInfo");
17 | resultDocument.Add(root);
18 |
19 | var nodeList = sourceString.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries).ToList();
20 |
21 | for (var index = 0; index < nodeList.Count; index++)
22 | {
23 | var node = nodeList[index];
24 | if (node.Contains("|")) continue;
25 | var text = node.Replace("+", "").Trim();
26 | text = CreateFriendlyName(text);
27 | var rootNode = new XElement(text);
28 | root.Add(rootNode);
29 | PopulateChildrenFromNode(nodeList.Skip(index + 1).ToList(), node, rootNode, 1);
30 | }
31 |
32 | return resultDocument;
33 | }
34 |
35 | private void PopulateChildrenFromNode(List followingBuffer, string thisNode, XContainer root,
36 | int depthSeek)
37 | {
38 | var prev = root;
39 |
40 | for (var index = 0; index < followingBuffer.Count; index++)
41 | {
42 | var childBufferNode = followingBuffer[index];
43 | var indexOfPlusSign = childBufferNode.IndexOf("+", StringComparison.Ordinal);
44 |
45 | if (indexOfPlusSign == depthSeek)
46 | {
47 | var nodeData = GetNodeKeyPairValue(childBufferNode);
48 | var newNode = new XElement(CreateFriendlyName(nodeData.Item1)) {Value = nodeData.Item2};
49 | root.Add(newNode);
50 |
51 | // Update the previous pointer
52 | prev = newNode;
53 | }
54 | else if (indexOfPlusSign == depthSeek + 1)
55 | {
56 | // you're a child, recurse down
57 | var newBuffer = followingBuffer.Skip(index).ToList();
58 |
59 |
60 | // Your buffer should only contain things that are not at or below your level...
61 | var i = 0;
62 | for (i = 0; i < newBuffer.Count; i++)
63 | {
64 | var s = newBuffer[i];
65 | if (s.IndexOf("+", StringComparison.Ordinal) < indexOfPlusSign)
66 | {
67 | break;
68 | }
69 | }
70 |
71 | newBuffer = newBuffer.Take(i).ToList();
72 |
73 | PopulateChildrenFromNode(newBuffer, childBufferNode, prev, depthSeek + 1);
74 |
75 | // Seek in based on the run length...
76 | var offset = GetPlusSignRunLengthOfDepth(newBuffer, depthSeek + 1);
77 | index += offset - 1;
78 |
79 | }
80 | else if(indexOfPlusSign == depthSeek - 1)
81 | {
82 | // you're leaving your parent, it's time to move on with your life :)
83 | break;
84 | }
85 | }
86 | }
87 |
88 | private Tuple GetNodeKeyPairValue(string node)
89 | {
90 | var splits = node.Split(":".ToCharArray(), 2);
91 | var key = splits[0];
92 |
93 | var value = "";
94 | if (splits.Length > 1)
95 | {
96 | value = splits[1];
97 | }
98 |
99 | return new Tuple(key.Trim(), value.Trim());
100 | }
101 |
102 | private string CreateFriendlyName(string input)
103 | {
104 | var pascal = input.Split(" ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).ToList();
105 | for (int index = 0; index < pascal.Count; index++)
106 | {
107 | var str = pascal[index];
108 | pascal[index] = ToUpper(str[0]) + str.Substring(1);
109 | }
110 |
111 | var newInput = string.Join("", pascal);
112 |
113 | return Regex.Replace(newInput,"[^A-Za-z _]","").Replace(" ", "_");
114 | }
115 |
116 | private int GetPlusSignRunLengthOfDepth(IEnumerable input, int target)
117 | {
118 | var count = 0;
119 | foreach (var x in input)
120 | {
121 | if (x.IndexOf("+", StringComparison.Ordinal) >= target)
122 | {
123 | count++;
124 | }
125 | else
126 | {
127 | return count;
128 | }
129 | }
130 |
131 | return count;
132 | }
133 |
134 | }
135 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | text/microsoft-resx
107 |
108 |
109 | 2.0
110 |
111 |
112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
113 |
114 |
115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/merge/SegmentTimecodeSelector.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Windows.Forms;
5 | using UnlinkMKV_GUI.data;
6 |
7 | namespace UnlinkMKV_GUI.merge
8 | {
9 | public class SegmentTimecodeSelector
10 | {
11 |
12 | public class SelectorDto
13 | {
14 | public SelectorDto(List segmentMap, List timecodes)
15 | {
16 | SegmentMap = segmentMap;
17 | Timecodes = timecodes;
18 | }
19 |
20 | public List SegmentMap { get; private set; }
21 | public List Timecodes { get; private set; }
22 | }
23 |
24 | private List _segMapping = new List();
25 | private List _timecodes = new List();
26 |
27 |
28 | public SelectorDto GetTimecodeAndSegments(MkvInfo info, IList segments, bool forceReorder)
29 | {
30 | this._segMapping.Clear();
31 | this._timecodes.Clear();
32 |
33 | // All chapters
34 | var englishChapters = info.Chapters.Editions[0].Chapters.ToList();
35 | englishChapters = englishChapters.Distinct().ToList();
36 |
37 | var increasesLinearly = AreChapterStartTimesLinearlyIncreasing(englishChapters);
38 |
39 | if (!increasesLinearly || forceReorder)
40 | {
41 | // This means that we'll have to reconstruct the timecodes based on the order in the file
42 | // as a best guess.. this would require MkvInfo and can be kind of dangerous as it assumes
43 | // all are sequential, but it's the best we can do in many cases.
44 | Console.WriteLine("Forced to reorder due to broken file...");
45 | englishChapters = ReOrderChaptersAndTimeCodes(segments, info, englishChapters);
46 | }
47 |
48 | SplitTimecode(info, segments,englishChapters);
49 |
50 | return new SelectorDto(this._segMapping, this._timecodes);
51 | }
52 |
53 | private void SplitTimecode(MkvInfo info, IList segments, IList chaptersInOrder)
54 | {
55 | var prev = "00:00:00.000000000";
56 |
57 | // This is the index that would have to be "added" onto... we'd always have at least one split
58 | var splitIndex = 1;
59 |
60 | foreach (var chapterAtom in chaptersInOrder)
61 | {
62 | // A split is required if you're linked; otherwise you can forget it
63 | if (chapterAtom.IsLinked())
64 | {
65 | if (prev != "00:00:00.000000000")
66 | {
67 | Console.WriteLine("segment to link!");
68 | this._timecodes.Add(prev);
69 | splitIndex++;
70 | }
71 |
72 | var segment = segments.First(x => x.Info.SegmentUid.IsSame(chapterAtom.ReferencedSegmentUid));
73 | this._segMapping.Add(segment.Filename);
74 | }
75 | else
76 | {
77 | prev = chapterAtom.ChapterTimecodeEnd;
78 | Console.WriteLine("split - original");
79 |
80 | // We check the last string to handle the case where the user might have decided
81 | // to place chapters next to each other, even if they are adjacenet in the split
82 | // which would happen if the episode is split into two parts (Part A + Part B, for example)
83 | var filename = $"splits/split-{splitIndex:D3}.mkv"; // for 3 digit splits
84 | var lastFilename = this._segMapping.LastOrDefault();
85 |
86 | if (filename != lastFilename)
87 | {
88 | this._segMapping.Add(filename);
89 | }
90 |
91 | }
92 | }
93 | }
94 |
95 | private bool AreChapterStartTimesLinearlyIncreasing(IList chapters)
96 | {
97 | var prev = TimeCodeUtil.TimeCodeToTimespan(chapters.First().ChapterTimecodeStart);
98 | foreach (var chapter in chapters)
99 | {
100 | var timeSpan = TimeCodeUtil.TimeCodeToTimespan(chapter.ChapterTimecodeStart);
101 | if (timeSpan < prev)
102 | {
103 | return false;
104 | }
105 | prev = timeSpan;
106 | }
107 | return true;
108 | }
109 |
110 | private List ReOrderChaptersAndTimeCodes(IList segmentParts, MkvInfo baseInfo, List chapters)
111 | {
112 | var prev = new TimeSpan();
113 | foreach (var chapter in chapters)
114 | {
115 | // Base info unless we say otherwise...
116 | MkvInfo info = null;
117 |
118 | if (chapter.ReferencedSegmentUid != null)
119 | {
120 | var segment = segmentParts.First(x => x.Info.SegmentUid.IsSame(chapter.ReferencedSegmentUid));
121 | info = segment.Info;
122 |
123 | chapter.ChapterTimecodeStart = TimeCodeUtil.TimespanToTimeCode(prev);
124 | prev = prev.Add(info.Duration);
125 | chapter.ChapterTimecodeEnd = TimeCodeUtil.TimespanToTimeCode(prev);
126 | }
127 | else
128 | {
129 | Console.WriteLine("Skipping a chapter from main file; it is presumed to be correct... but it may not be~!");
130 |
131 | // Hand over; hopefully it's correct...
132 | prev = TimeCodeUtil.TimeCodeToTimespan(chapter.ChapterTimecodeEnd);
133 | }
134 | }
135 |
136 | return chapters;
137 | }
138 | }
139 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/FormValidator.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/FormApplication.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/ValidationStatusControl.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **You can download up at the top under 'releases'!**
2 |
3 |
4 | # What is it?
5 |
6 |
7 | Provides a GUI interface for the UnlinkMKV project by Garret Noling. Modified to run cross-platform and provide unified support over many different operating systems.
8 |
9 | UnlinkMKV: https://github.com/gnoling/UnlinkMKV
10 |
11 | Written with C#, runs on Mono for many different operating systems.
12 |
13 |
14 | **Please keep in mind the application can be run into two modes. If you want to use the Perl "UnlinkMKV" backend, then do NOT tick off "Native Mode" on the GUI. If you are running into problems with that mode or some of the outputs it produces, try ticking "Native Mode" and running the process again. In some cases, this will resolve some of the issues.**
15 |
16 | # Dependencies
17 |
18 | The following are required on every platform:
19 |
20 | * Perl (required)
21 | * FFmpeg (optional, encoding)
22 | * MKVToolnix (required)
23 | * Mediainfo (Native Backend)
24 | * Mono or .NET Framework
25 |
26 | Following the below directions for getting the depedencies you might need:
27 |
28 | ## Windows
29 |
30 | **Perl**: Strawberry Perl has been tested on Windows and works with this application. If you already have some Perl version of some sort installed, it will probably work and you can skip this. Otherwise, install Strawberry Perl from http://strawberryperl.com/ The latest version will be fine.
31 |
32 | **MKVToolnix**: Install from here and use the installer https://www.bunkus.org/videotools/mkvtoolnix/downloads.html (or you can download the ZIP and add it to your PATH, if you prefer). Copy and note the install path you are putting down
33 |
34 | **FFmpeg**: This is optional, only required if you want to encode but you can find many guides on the internet to setting it up, such as this: http://jonhall.info/how_to/setup_and_use_ffmpeg_on_windows
35 |
36 | **Mediainfo**: This is optional as well but is needed if you want to use "Native Mode" You need to make sure you have the command line utilities available to use. For Windows, you can download those here: https://mediaarea.net/en/MediaInfo/Download/Windows
37 |
38 | **.NET**: You should already have this.
39 |
40 | If you get issues with "MKVToolnix" not found, you'll need to add the MKVToolnix to your PATH as well. The application will try and out detect it and will succeed if you use the above installer version.
41 |
42 | Otherwise, do this:
43 |
44 | Select Computer from the Start menu (or hold Windows key and press Break), choose "Advanced System Settings", then the Advanced tab. Click on Environment Variables. Under System Variables, find PATH, and click on it. In the Edit window, modify PATH by adding a semicolon ";" at the end and then the path to your MKVtoolnix installation, e.g. C:\Program Files (x86)\MKVToolNix (wherever you installed it to, this should be different for different drive letters or 64 bit)
45 |
46 |
47 |
48 | ## OSX
49 |
50 | **Perl**: This is already installed for you
51 | **MKVToolnix**: You can install it from Homebrew. If you don't have Homebrew yet, run `ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"`
52 |
53 | Then, you should be able to run:
54 |
55 | `brew install -vb --with-flac mkvtoolnix`
56 |
57 | **Fmpeg**: Run with Brew, `brew install ffmpeg --with-fdk-aac --with-ffplay --with-freetype --with-frei0r --with-libass --with-libvo-aacenc --with-libvorbis --with-libvpx --with-opencore-amr --with-openjpeg --with-opus --with-rtmpdump --with-schroedinger --with-speex --with-theora --with-tools`
58 |
59 | **Mono**: `brew install mono` to install Mono from Brew.
60 |
61 | **About Homebrew**: You will require the XCode Command Line Tools to use Homebrew; you can find a guide on that here: http://railsapps.github.io/xcode-command-line-tools.html
62 |
63 | You can then run the application with `mono UnlinkMKV-GUI.exe` or follow these directions to make it clickable in Finder: http://superuser.com/questions/67126/how-to-associate-the-exe-extension-to-be-opened-with-mono
64 |
65 | ## Linux
66 |
67 | If you're running on Linux, you will need to look up how to get the tools above from your local package repository via your package manager from your distribution of choice.
68 |
69 | ## Every Operating System
70 |
71 | Using Perl backend:
72 |
73 | You will probably need `Log::Log4perl`, `XML::LibXML` and `String:CRC32` for your Perl installation. You can do this step just in case if you're not sure if it's installed.
74 |
75 | 1. Open a terminal or command prompt on your operating system
76 | 2. Type `cpan Log::Log4perl` and then press return/enter
77 | 3. Repeat step 2, but type `cpan XML::LibXML` instead
78 | 3. Repeat step 3, but type `cpan String:CRC32` instead
79 | 4. The CPAN manager should install the logging module, and the XML module, you should be able to exit the terminal now.
80 |
81 | If you are using Mono, the first run always takes a little bit (about a minute). Please be patient.
82 |
83 | Native Backend:
84 |
85 | There is no need for Perl. Just note that the native backend is less compatible and more compatible in some cases. If you have troublesome files, you may want to try both.
86 |
87 | # Using the application
88 |
89 | After installing all the depedencies, just run the application.
90 |
91 | * Input Folder: Just select where the files you want to unlink are and they will be handled
92 | * Output Folder: Just select where you want the files to be placed
93 |
94 | The check boxes should be left unchecked for the most part unless you have a particular issue with a release. You can play around with them and file a bug report if a certain MKV file is not working.
95 |
96 | If you get "unlinked finished" immediately and no output, make sure you have installed the Perl logging module. See the section above for 'Every operating system'
97 |
98 | # I found a bug/the application doesn't Unlink my MKV's properly. Help?
99 |
100 | You can post the issue with a verbose log on the issue tracker. Tick the "verbose output" checkbox in the options when doing the run before posting a log on the issue tracked. If the issue is part of the UnlinkMKV core, it will be addressed there. Otherwise, it will be addressed here.
101 |
102 | If the issue is with a particular release or set of files, please open an issue for that particular for that release.
103 |
104 | # For developers
105 |
106 | Pull requests are appreciated. Please feel free to submit them.
107 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/UnlinkMKV-GUI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {31A91B00-390D-43D6-9655-633503D1821E}
8 | WinExe
9 | Properties
10 | UnlinkMKV_GUI
11 | UnlinkMKV-GUI
12 | v4.5
13 | 512
14 |
15 |
16 | AnyCPU
17 | true
18 | full
19 | false
20 | bin\Debug\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 |
25 |
26 | AnyCPU
27 | pdbonly
28 | true
29 | bin\Release\
30 | TRACE
31 | prompt
32 | 4
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | Form
57 |
58 |
59 | FormApplication.cs
60 |
61 |
62 | Form
63 |
64 |
65 | FormValidator.cs
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | UserControl
91 |
92 |
93 | ValidationStatusControl.cs
94 |
95 |
96 | FormApplication.cs
97 |
98 |
99 | FormValidator.cs
100 |
101 |
102 | ResXFileCodeGenerator
103 | Resources.Designer.cs
104 | Designer
105 |
106 |
107 | True
108 | Resources.resx
109 |
110 |
111 | ValidationStatusControl.cs
112 |
113 |
114 | SettingsSingleFileGenerator
115 | Settings.Designer.cs
116 |
117 |
118 | True
119 | Settings.settings
120 | True
121 |
122 |
123 | PreserveNewest
124 |
125 |
126 |
127 |
128 |
129 |
130 |
137 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/merge/MergeJob.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using UnlinkMKV_GUI.data;
8 | using UnlinkMKV_GUI.merge.extract;
9 | using UnlinkMKV_GUI.merge.info;
10 |
11 | namespace UnlinkMKV_GUI.merge
12 | {
13 |
14 | public class MergeJob : IMkvJobMerger
15 | {
16 | private readonly MergeOptions _options;
17 | private string _filename;
18 | private string _destination;
19 | private string _baseFilename;
20 | private List _timecodes = new List();
21 | private List _segMapping = new List();
22 |
23 | public MergeJob(MergeOptions options)
24 | {
25 | _options = options;
26 | }
27 |
28 | public async Task ExecuteJob(TextWriter logger, string source, string destination)
29 | {
30 | this._filename = source;
31 | this._destination = destination;
32 | this._timecodes.Clear();
33 | this._segMapping.Clear();
34 |
35 | return PerformMerge();
36 | }
37 |
38 | public MergeResult PerformMerge()
39 | {
40 | Console.WriteLine("Starting the processing of the file: {0}", _filename);
41 | _baseFilename = Path.GetFileName(_filename);
42 |
43 | var loader = new MkvToolNixMkvInfoLoaderStrategy();
44 | var info = loader.FetchMkvInfo(_filename);
45 |
46 | if (info.IsFileLinked())
47 | {
48 | var workingDir = PathUtil.GetTemporaryDirectory();
49 | Environment.CurrentDirectory = workingDir;
50 |
51 | try
52 | {
53 | Console.WriteLine("File was deteremined to be linked so will begin process...");
54 | Console.WriteLine($"Your working directory will be: {workingDir}");
55 |
56 | // Step 1: Capture the segments that are the same in the directory
57 | var segments = SegmentUtility.GetMergePartsForFilename(_filename, info);
58 |
59 | // Step 2: Extract all attachments from the current segments ++ extract out the main attachments, too
60 | // segments.ForEach(ExtractAttachments);
61 |
62 | var selector = new SegmentTimecodeSelector();
63 | var dto = selector.GetTimecodeAndSegments(info, segments, false);
64 | this._segMapping = dto.SegmentMap;
65 | this._timecodes = dto.Timecodes;
66 |
67 | // Step 4: Extract the splits
68 | ExtractSplits(_filename);
69 |
70 | // Step 5: Let's rebuild the file... the timecodes can tell us quite a bit but let's use the fully
71 | // built "order"
72 | RebuildFile();
73 |
74 | File.Move(Path.Combine(workingDir, _baseFilename), Path.Combine(_destination, _baseFilename));
75 | Console.WriteLine("Job complete!");
76 | }
77 | catch (Exception e)
78 | {
79 | throw;
80 | }
81 | finally
82 | {
83 | // Clean up after ones self
84 | Directory.Delete(workingDir, true);
85 | }
86 | }
87 | else
88 | {
89 | Console.WriteLine("File was determined to not be linked. Ignoring.");
90 | return MergeResult.NothingToMerge;
91 | }
92 |
93 | return MergeResult.OK;
94 | }
95 |
96 | private void RebuildFile()
97 | {
98 |
99 | var proc = new ProcessStartInfo("mkvmerge");
100 |
101 | var y = new List();
102 | foreach (var s in _segMapping)
103 | {
104 | y.Add(string.Format("\"{0}\"", s));
105 | }
106 |
107 | // Removing chapters options helps with the following issues:
108 | // 1. Ordered flags will get left behind sometimes, which is a nuissance
109 | // 2. Artifacted chapters are rarely useful when played back in something like Plex
110 | var parts = string.Join(" --no-chapters +", y);
111 |
112 | proc.Arguments = $"--no-chapters -o {_baseFilename} {parts}";
113 | var process = new Process();
114 | process.StartInfo = proc;
115 | process.Start();
116 | process.WaitForExit();
117 | }
118 |
119 | private int TimecodeComparator(ChapterAtom x, ChapterAtom y)
120 | {
121 | var firstStart = TimeCodeUtil.TimeCodeToTimespan(x.ChapterTimecodeStart);
122 | var secondStart = TimeCodeUtil.TimeCodeToTimespan(y.ChapterTimecodeStart);
123 |
124 | return firstStart.CompareTo(secondStart);
125 |
126 | }
127 |
128 | private void ExtractAttachments(MergePart segment)
129 | {
130 | using (var extractor = new AttachmentExtractor())
131 | {
132 | extractor.PerformExtraction(segment.Info, segment.Filename);
133 | }
134 | }
135 |
136 | private static string CreateAndChangeTo(string request)
137 | {
138 | // OK, change directory to make sure things are outputted there
139 | var oldPath = Environment.CurrentDirectory;
140 | var newPath = Path.Combine(oldPath, request);
141 | Directory.CreateDirectory(newPath);
142 | Environment.CurrentDirectory = newPath;
143 | return oldPath;
144 | }
145 |
146 | private void ExtractSplits(string baseFilename)
147 | {
148 | var oldPath = CreateAndChangeTo("splits");
149 |
150 |
151 | if (this._timecodes.Any())
152 | {
153 | var joined = string.Join(",", this._timecodes);
154 | Console.WriteLine(joined);
155 | var p = Process.Start("mkvmerge",
156 | $"--no-chapters -o split-%03d.mkv \"{this._filename}\" --split timecodes:{joined}");
157 |
158 | p.WaitForExit();
159 | }
160 | else
161 | {
162 | // Must be sandwhiched and have no chances for splits... use the base
163 | File.Copy(baseFilename, "split-001.mkv");
164 | }
165 |
166 | Environment.CurrentDirectory = oldPath;
167 | }
168 |
169 | }
170 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/FormApplication.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using System.Windows.Forms;
3 | using System;
4 | using System.Threading;
5 | using System.Diagnostics;
6 | using System.IO;
7 | using System.Collections.Generic;
8 | using System.Text.RegularExpressions;
9 | using UnlinkMKV_GUI.legacy;
10 | using UnlinkMKV_GUI.merge;
11 | using UnlinkMKV_GUI.ui;
12 |
13 | namespace UnlinkMKV_GUI {
14 | public partial class FormApplication : Form {
15 | // This is a task run by UnlinkMKV with the intention of performing batch jobs
16 | private Thread _taskThread;
17 |
18 | public FormApplication() {
19 | InitializeComponent();
20 |
21 | textOutput.Text = Properties.Settings.Default["output"].ToString();
22 | textInput.Text = Properties.Settings.Default["input"].ToString();
23 |
24 |
25 | // Create the default path if needed
26 | if (string.IsNullOrEmpty(textOutput.Text))
27 | {
28 | string defaultPath = Path.Combine(Environment.CurrentDirectory, "output");
29 | Directory.CreateDirectory(defaultPath);
30 | textOutput.Text = defaultPath;
31 | }
32 |
33 | // Create our options
34 | CreateOptions();
35 | }
36 |
37 | private void CreateOptions() {
38 | // Setup a list of tuples for the options that matter
39 |
40 | var optionList = new List>();
41 |
42 | bool foundMpeg = true;
43 |
44 | try {
45 | PathUtility.FindExePath("ffmpeg.exe");
46 | }
47 | catch (FileNotFoundException exception) {
48 | foundMpeg = false;
49 | }
50 |
51 | if (foundMpeg)
52 | {
53 | optionList.Add(Tuple.Create("Fix audio", "--fixaudio"));
54 | optionList.Add(Tuple.Create("Fix video", "--fixvideo"));
55 | }
56 |
57 | optionList.Add(Tuple.Create("Fix subtitles", "--fixsubtitles"));
58 | optionList.Add(Tuple.Create("Ignore default flag", "--ignoredefaultflag"));
59 | optionList.Add(Tuple.Create("Ignore missing segments", "--ignoremissingsegments"));
60 |
61 | optionList.Add(Tuple.Create("Verbose output", "--ll TRACE"));
62 | optionList.Add(Tuple.Create("Native Backend", "--native"));
63 |
64 | foreach (var option in optionList) {
65 | var checkBox = new CheckBox();
66 | checkBox.Text = option.Item1;
67 | checkBox.Tag = option.Item2;
68 | checkBox.AutoSize = true;
69 | flowLayoutPanel1.Controls.Add(checkBox);
70 | }
71 |
72 | }
73 |
74 | private void FormApplication_Load(object sender, EventArgs e) {
75 |
76 | // Open the validator
77 | var validator = new FormValidator();
78 | validator.ShowDialog();
79 | }
80 |
81 | private void buttonInput_Click(object sender, EventArgs e) {
82 | BrowseForFile(textInput);
83 | }
84 |
85 | private void buttonOutput_Click(object sender, EventArgs e) {
86 | BrowseForFile(textOutput);
87 | }
88 |
89 | private void BrowseForFile(TextBox textToUpdate) {
90 |
91 | var browse = new FolderBrowserDialog();
92 | var result = browse.ShowDialog();
93 |
94 | if (result == DialogResult.OK)
95 | {
96 | textToUpdate.Text = browse.SelectedPath;
97 | }
98 |
99 | }
100 |
101 | private string GetCommandLineArguments() {
102 | string buffer = "";
103 |
104 | foreach (var checkbox in flowLayoutPanel1.Controls.OfType()) {
105 | if (checkbox.Checked)
106 | {
107 | buffer += checkbox.Tag + " ";
108 | }
109 | }
110 | return buffer;
111 | }
112 |
113 | private bool ShouldUseNativeBackend()
114 | {
115 | return GetCommandLineArguments().IndexOf("native", StringComparison.Ordinal) > -1;
116 | }
117 |
118 | private void buttonAbort_Click(object sender, EventArgs e) {
119 | // Abort the thread immediately
120 | _taskThread.Abort();
121 |
122 | // Force a cleanup of all stray processes that might be around
123 | var processToKill = GetUtilityPrograms();
124 | processToKill.ForEach(x => Process.GetProcessesByName(x).ToList().ForEach(process => process.Kill()));
125 |
126 | _currProcess?.Kill();
127 |
128 | buttonExecute.Enabled = true;
129 | buttonAbort.Enabled = false;
130 |
131 | }
132 |
133 | private List GetUtilityPrograms() {
134 | return new List() { "mkvmerge", "mkvinfo", "mkvextract", "ffmpeg", "mediainfo" };
135 | }
136 |
137 | private void buttonExecute_Click(object sender, EventArgs e) {
138 | buttonExecute.Enabled = false;
139 | buttonAbort.Enabled = true;
140 |
141 | if (VerifyReady())
142 | {
143 | _taskThread = new Thread(PerformJob);
144 | _taskThread.Start();
145 | }
146 | }
147 |
148 | private Process _currProcess;
149 |
150 | private bool VerifyReady() {
151 | bool ready = true;
152 |
153 |
154 | if (!Directory.Exists(textOutput.Text) || !Directory.Exists(textInput.Text))
155 | {
156 | ready = false;
157 | MessageBox.Show("Make sure to select some input and output paths that are valid before unlinking.");
158 | }
159 |
160 | return ready;
161 | }
162 |
163 |
164 | private async void PerformJob()
165 | {
166 | var isNative = ShouldUseNativeBackend();
167 |
168 | // Clear log
169 | textLog.Clear();
170 |
171 | // Create directory ahead of time
172 | Directory.CreateDirectory(textOutput.Text);
173 |
174 | IMkvJobMerger mergeStrategy;
175 | var logger = new TextBoxControlWriter(this, textLog);
176 |
177 | if (isNative)
178 | {
179 | mergeStrategy = new MergeJob(new MergeOptions());
180 | Console.SetOut(logger);
181 | }
182 | else
183 | {
184 | mergeStrategy = new PerlJob(GetCommandLineArguments());
185 | }
186 |
187 | var sourcePath = textInput.Text;
188 | var files = Directory.GetFiles(sourcePath);
189 |
190 | foreach (var file in files)
191 | {
192 | var result = mergeStrategy.ExecuteJob(logger, file, textOutput.Text);
193 | }
194 |
195 | // End the job
196 | ExecuteSecure(() => MessageBox.Show("The unlinking is complete!", "Complete", MessageBoxButtons.OK, MessageBoxIcon.Information));
197 |
198 | buttonExecute.Enabled = true;
199 | buttonAbort.Enabled = false;
200 | }
201 |
202 | private void ExecuteSecure(Action a) {
203 | if (InvokeRequired)
204 | BeginInvoke(a);
205 | else
206 | a();
207 | }
208 |
209 | private void FormApplication_FormClosing(object sender, FormClosingEventArgs e) {
210 | if (_taskThread != null && _taskThread.IsAlive)
211 | {
212 | e.Cancel = true;
213 | MessageBox.Show(
214 | "The conversion process is not yet complete. Please abort the current job first before closing.",
215 | "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
216 | }
217 |
218 | // Save settings
219 | Properties.Settings.Default["input"] = textInput.Text;
220 | Properties.Settings.Default["output"] = textOutput.Text;
221 |
222 | // Save the settings to the disk
223 | Properties.Settings.Default.Save();
224 |
225 | }
226 |
227 | }
228 |
229 | ///
230 | /// A basic config file that is used to read and write to the config file provided by UnlinkMKV
231 | ///
232 | public class Config {
233 | private Dictionary _values = new Dictionary();
234 |
235 | public Config(string fileName) {
236 | using (TextReader reader = File.OpenText(fileName)) {
237 | string line = null;
238 | while ((line = reader.ReadLine()) != null) {
239 | if (line == null) {
240 | break;
241 | }
242 | // Now, chomp each line down
243 | string[] chomped = line.Trim().Split("=".ToCharArray());
244 | string key = chomped[0].Trim();
245 | string value = chomped[1].Trim();
246 | _values.Add(key, value);
247 | }
248 | }
249 |
250 | // The temporary path should just be set to what's normal for you
251 | _values["tmpdir"] = Path.Combine(Path.GetTempPath(), "UnlinkMKV");
252 |
253 | //
254 | // ffmpeg = $basedir/ffmpeg/bin/ffmpeg
255 | // mkvext = $basedir/mkvtoolnix/mkvextract
256 | // mkvinfo = $basedir/mkvtoolnix/mkvinfo
257 | // mkvmerge = $basedir/mkvtoolnix/mkvmerge
258 |
259 | }
260 |
261 | ///
262 | /// This function should be invoked to set the paths properly when the config is being constructed.1
263 | ///
264 | ///
265 | ///
266 | ///
267 | ///
268 | public void SetRequiredPaths(string ffmpeg, string mkvext, string mkvinfo, string mkvmerge) {
269 | _values["ffmpeg"] = ffmpeg;
270 | _values["mkvext"] = mkvext;
271 | _values["mkvinfo"] = mkvinfo;
272 | _values["mkvmerge"] = mkvmerge;
273 | }
274 |
275 | public string OutputPath {
276 | get {
277 | return _values["out_dir"];
278 | }
279 | set {
280 | _values["out_dir"] = value;
281 | }
282 | }
283 |
284 | // TODO; Add the rest of the flags in here when it's able
285 |
286 | public void Persist(string fileName) {
287 | // Delete the file prior (mono bug?)
288 | File.Delete(fileName);
289 |
290 | var fs = new FileStream(fileName, FileMode.Create, FileAccess.Write);
291 | using (var writer = new StreamWriter(fs)) {
292 | foreach (var entry in _values) {
293 | string lineEntry = string.Format("{0} = {1}", entry.Key, entry.Value);
294 | writer.WriteLine(lineEntry);
295 | }
296 | }
297 |
298 | // stream writer is now closed at this point in time; all set!
299 | }
300 |
301 | }
302 |
303 | }
304 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/FormApplication.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace UnlinkMKV_GUI
2 | {
3 | partial class FormApplication
4 | {
5 | ///
6 | /// Required designer variable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Clean up any resources being used.
12 | ///
13 | /// true if managed resources should be disposed; otherwise, false.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Windows Form Designer generated code
24 |
25 | ///
26 | /// Required method for Designer support - do not modify
27 | /// the contents of this method with the code editor.
28 | ///
29 | private void InitializeComponent()
30 | {
31 | this.label1 = new System.Windows.Forms.Label();
32 | this.textInput = new System.Windows.Forms.TextBox();
33 | this.buttonInput = new System.Windows.Forms.Button();
34 | this.buttonOutput = new System.Windows.Forms.Button();
35 | this.textOutput = new System.Windows.Forms.TextBox();
36 | this.label2 = new System.Windows.Forms.Label();
37 | this.groupBox1 = new System.Windows.Forms.GroupBox();
38 | this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel();
39 | this.groupBox2 = new System.Windows.Forms.GroupBox();
40 | this.textLog = new System.Windows.Forms.RichTextBox();
41 | this.buttonExecute = new System.Windows.Forms.Button();
42 | this.buttonAbort = new System.Windows.Forms.Button();
43 | this.buttonIssues = new System.Windows.Forms.Button();
44 | this.label3 = new System.Windows.Forms.Label();
45 | this.groupBox1.SuspendLayout();
46 | this.groupBox2.SuspendLayout();
47 | this.SuspendLayout();
48 | //
49 | // label1
50 | //
51 | this.label1.AutoSize = true;
52 | this.label1.Location = new System.Drawing.Point(8, 23);
53 | this.label1.Name = "label1";
54 | this.label1.Size = new System.Drawing.Size(63, 13);
55 | this.label1.TabIndex = 0;
56 | this.label1.Text = "Input Folder";
57 | //
58 | // textInput
59 | //
60 | this.textInput.Enabled = false;
61 | this.textInput.Location = new System.Drawing.Point(77, 19);
62 | this.textInput.Name = "textInput";
63 | this.textInput.Size = new System.Drawing.Size(350, 20);
64 | this.textInput.TabIndex = 1;
65 | //
66 | // buttonInput
67 | //
68 | this.buttonInput.Location = new System.Drawing.Point(433, 17);
69 | this.buttonInput.Name = "buttonInput";
70 | this.buttonInput.Size = new System.Drawing.Size(75, 23);
71 | this.buttonInput.TabIndex = 2;
72 | this.buttonInput.Text = "...";
73 | this.buttonInput.UseVisualStyleBackColor = true;
74 | this.buttonInput.Click += new System.EventHandler(this.buttonInput_Click);
75 | //
76 | // buttonOutput
77 | //
78 | this.buttonOutput.Location = new System.Drawing.Point(433, 48);
79 | this.buttonOutput.Name = "buttonOutput";
80 | this.buttonOutput.Size = new System.Drawing.Size(75, 23);
81 | this.buttonOutput.TabIndex = 6;
82 | this.buttonOutput.Text = "...";
83 | this.buttonOutput.UseVisualStyleBackColor = true;
84 | this.buttonOutput.Click += new System.EventHandler(this.buttonOutput_Click);
85 | //
86 | // textOutput
87 | //
88 | this.textOutput.Enabled = false;
89 | this.textOutput.Location = new System.Drawing.Point(77, 50);
90 | this.textOutput.Name = "textOutput";
91 | this.textOutput.Size = new System.Drawing.Size(350, 20);
92 | this.textOutput.TabIndex = 5;
93 | //
94 | // label2
95 | //
96 | this.label2.AutoSize = true;
97 | this.label2.Location = new System.Drawing.Point(8, 54);
98 | this.label2.Name = "label2";
99 | this.label2.Size = new System.Drawing.Size(71, 13);
100 | this.label2.TabIndex = 4;
101 | this.label2.Text = "Output Folder";
102 | //
103 | // groupBox1
104 | //
105 | this.groupBox1.Controls.Add(this.flowLayoutPanel1);
106 | this.groupBox1.Location = new System.Drawing.Point(12, 92);
107 | this.groupBox1.Name = "groupBox1";
108 | this.groupBox1.Size = new System.Drawing.Size(527, 132);
109 | this.groupBox1.TabIndex = 7;
110 | this.groupBox1.TabStop = false;
111 | this.groupBox1.Text = "Options";
112 | //
113 | // flowLayoutPanel1
114 | //
115 | this.flowLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
116 | this.flowLayoutPanel1.FlowDirection = System.Windows.Forms.FlowDirection.TopDown;
117 | this.flowLayoutPanel1.Location = new System.Drawing.Point(3, 16);
118 | this.flowLayoutPanel1.Margin = new System.Windows.Forms.Padding(13);
119 | this.flowLayoutPanel1.Name = "flowLayoutPanel1";
120 | this.flowLayoutPanel1.Size = new System.Drawing.Size(521, 113);
121 | this.flowLayoutPanel1.TabIndex = 0;
122 | //
123 | // groupBox2
124 | //
125 | this.groupBox2.Controls.Add(this.textLog);
126 | this.groupBox2.Location = new System.Drawing.Point(12, 241);
127 | this.groupBox2.Name = "groupBox2";
128 | this.groupBox2.Size = new System.Drawing.Size(527, 147);
129 | this.groupBox2.TabIndex = 8;
130 | this.groupBox2.TabStop = false;
131 | this.groupBox2.Text = "Output Log";
132 | //
133 | // textLog
134 | //
135 | this.textLog.Dock = System.Windows.Forms.DockStyle.Fill;
136 | this.textLog.Location = new System.Drawing.Point(3, 16);
137 | this.textLog.Name = "textLog";
138 | this.textLog.ReadOnly = true;
139 | this.textLog.Size = new System.Drawing.Size(521, 128);
140 | this.textLog.TabIndex = 0;
141 | this.textLog.Text = "";
142 | //
143 | // buttonExecute
144 | //
145 | this.buttonExecute.Location = new System.Drawing.Point(430, 421);
146 | this.buttonExecute.Name = "buttonExecute";
147 | this.buttonExecute.Size = new System.Drawing.Size(75, 23);
148 | this.buttonExecute.TabIndex = 9;
149 | this.buttonExecute.Text = "Unlink";
150 | this.buttonExecute.UseVisualStyleBackColor = true;
151 | this.buttonExecute.Click += new System.EventHandler(this.buttonExecute_Click);
152 | //
153 | // buttonAbort
154 | //
155 | this.buttonAbort.Enabled = false;
156 | this.buttonAbort.Location = new System.Drawing.Point(349, 421);
157 | this.buttonAbort.Name = "buttonAbort";
158 | this.buttonAbort.Size = new System.Drawing.Size(75, 23);
159 | this.buttonAbort.TabIndex = 10;
160 | this.buttonAbort.Text = "Abort";
161 | this.buttonAbort.UseVisualStyleBackColor = true;
162 | this.buttonAbort.Click += new System.EventHandler(this.buttonAbort_Click);
163 | //
164 | // buttonIssues
165 | //
166 | this.buttonIssues.Enabled = false;
167 | this.buttonIssues.Location = new System.Drawing.Point(15, 421);
168 | this.buttonIssues.Name = "buttonIssues";
169 | this.buttonIssues.Size = new System.Drawing.Size(119, 23);
170 | this.buttonIssues.TabIndex = 11;
171 | this.buttonIssues.Text = "Known MKV issues";
172 | this.buttonIssues.UseVisualStyleBackColor = true;
173 | //
174 | // label3
175 | //
176 | this.label3.AutoSize = true;
177 | this.label3.Location = new System.Drawing.Point(34, 400);
178 | this.label3.Name = "label3";
179 | this.label3.Size = new System.Drawing.Size(484, 13);
180 | this.label3.TabIndex = 1;
181 | this.label3.Text = "Please note: Input and output folders on network paths (unmapped networks) are un" +
182 | "supported. Sorry!";
183 | //
184 | // FormApplication
185 | //
186 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
187 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
188 | this.ClientSize = new System.Drawing.Size(551, 456);
189 | this.Controls.Add(this.label3);
190 | this.Controls.Add(this.buttonIssues);
191 | this.Controls.Add(this.buttonAbort);
192 | this.Controls.Add(this.buttonExecute);
193 | this.Controls.Add(this.groupBox2);
194 | this.Controls.Add(this.groupBox1);
195 | this.Controls.Add(this.buttonOutput);
196 | this.Controls.Add(this.textOutput);
197 | this.Controls.Add(this.label2);
198 | this.Controls.Add(this.buttonInput);
199 | this.Controls.Add(this.textInput);
200 | this.Controls.Add(this.label1);
201 | this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
202 | this.MaximizeBox = false;
203 | this.Name = "FormApplication";
204 | this.ShowIcon = false;
205 | this.Text = "UnlinkMKV-GUI";
206 | this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.FormApplication_FormClosing);
207 | this.Load += new System.EventHandler(this.FormApplication_Load);
208 | this.groupBox1.ResumeLayout(false);
209 | this.groupBox2.ResumeLayout(false);
210 | this.ResumeLayout(false);
211 | this.PerformLayout();
212 |
213 | }
214 |
215 | #endregion
216 |
217 | private System.Windows.Forms.Label label1;
218 | private System.Windows.Forms.TextBox textInput;
219 | private System.Windows.Forms.Button buttonInput;
220 | private System.Windows.Forms.Button buttonOutput;
221 | private System.Windows.Forms.TextBox textOutput;
222 | private System.Windows.Forms.Label label2;
223 | private System.Windows.Forms.GroupBox groupBox1;
224 | private System.Windows.Forms.GroupBox groupBox2;
225 | private System.Windows.Forms.Button buttonExecute;
226 | private System.Windows.Forms.Button buttonAbort;
227 | private System.Windows.Forms.Button buttonIssues;
228 | private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel1;
229 | private System.Windows.Forms.RichTextBox textLog;
230 | private System.Windows.Forms.Label label3;
231 | }
232 | }
233 |
234 |
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI.Tests/data/XmlMkvInfoSummaryMapperTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using UnlinkMKV_GUI.data;
3 | using UnlinkMKV_GUI.data.xml;
4 | using Xunit;
5 | using Xunit.Abstractions;
6 |
7 | namespace UnlinkMKV_GUI.Tests.data
8 | {
9 |
10 | public class XmlMkvInfoSummaryMapperTest
11 | {
12 | private readonly ITestOutputHelper _helper;
13 |
14 | public XmlMkvInfoSummaryMapperTest(ITestOutputHelper helper)
15 | {
16 | _helper = helper;
17 | }
18 |
19 | [Fact]
20 | public void TestStandardMethod()
21 | {
22 | XmlMkvInfoSummaryMapper mapper = new XmlMkvInfoSummaryMapper();
23 | const string data = @"
24 | + EBML head
25 | |+ EBML version: 1
26 | |+ EBML read version: 1
27 | |+ EBML maximum ID length: 4
28 | |+ EBML maximum size length: 8
29 | |+ Doc type: matroska
30 | |+ Doc type version: 4
31 | |+ Doc type read version: 2
32 | + Segment, size 756458612
33 | |+ Seek head (subentries will be skipped)
34 | |+ EbmlVoid (size: 4013)
35 | |+ Segment information
36 | | + Timecode scale: 1000000
37 | | + Muxing application: libebml v1.3.0 + libmatroska v1.4.0
38 | | + Writing application: mkvmerge v6.0.0 ('Coming Up For Air') built on Jan 20 2013 09:52:00
39 | | + Duration: 1396.360s (00:23:16.360)
40 | | + Date: Mon Mar 18 23:40:26 2013 UTC
41 | | + Title: Nisemonogatari 01
42 | | + Segment UID: 0xbc 0xfe 0x9f 0x6d 0x5e 0xa3 0xa9 0x70 0x99 0x7c 0x32 0x17 0x3f 0x06 0x60 0x22
43 | |+ Segment tracks
44 | | + A track
45 | | + Track number: 1 (track ID for mkvmerge & mkvextract: 0)
46 | | + Track UID: 3740095644
47 | | + Track type: video
48 | | + Lacing flag: 0
49 | | + MinCache: 1
50 | | + Codec ID: V_MPEG4/ISO/AVC
51 | | + CodecPrivate, length 47 (h.264 profile: High 10 @L5.0)
52 | | + Default duration: 41.708ms (23.976 frames/fields per second for a video track)
53 | | + Language: und
54 | | + Name: Nisemonogatari 01
55 | | + Video track
56 | | + Pixel width: 1920
57 | | + Pixel height: 1080
58 | | + Display width: 1920
59 | | + Display height: 1080
60 | | + A track
61 | | + Track number: 2 (track ID for mkvmerge & mkvextract: 1)
62 | | + Track UID: 2576664210
63 | | + Track type: audio
64 | | + Codec ID: A_FLAC
65 | | + CodecPrivate, length 154
66 | | + Default duration: 85.333ms (11.719 frames/fields per second for a video track)
67 | | + Language: jpn
68 | | + Name: 2.0 FLAC
69 | | + Audio track
70 | | + Sampling frequency: 48000
71 | | + Channels: 2
72 | | + Bit depth: 16
73 | | + A track
74 | | + Track number: 3 (track ID for mkvmerge & mkvextract: 2)
75 | | + Track UID: 1317315370984360101
76 | | + Track type: subtitles
77 | | + Lacing flag: 0
78 | | + Codec ID: S_TEXT/ASS
79 | | + CodecPrivate, length 5185
80 | | + Name: English
81 | |+ EbmlVoid (size: 1120)
82 | |+ Attachments
83 | | + Attached
84 | | + File name: Hultog Italic.ttf
85 | | + Mime type: application/x-truetype-font
86 | | + File data, size: 37372
87 | | + File UID: 2445306772
88 | | + Attached
89 | | + File name: Hultog.ttf
90 | | + Mime type: application/x-truetype-font
91 | | + File data, size: 33296
92 | | + File UID: 2020957090
93 | | + Attached
94 | | + File name: AmazGoDa.ttf
95 | | + Mime type: application/x-truetype-font
96 | | + File data, size: 43044
97 | | + File UID: 3630482355
98 | | + Attached
99 | | + File name: AmazGoDaBold.ttf
100 | | + Mime type: application/x-truetype-font
101 | | + File data, size: 35076
102 | | + File UID: 4075561618
103 | | + Attached
104 | | + File name: Andyb.TTF
105 | | + Mime type: application/x-truetype-font
106 | | + File data, size: 70324
107 | | + File UID: 1824581507
108 | | + Attached
109 | | + File name: ANNA.ttf
110 | | + Mime type: application/x-truetype-font
111 | | + File data, size: 76000
112 | | + File UID: 642960046
113 | | + Attached
114 | | + File name: Arena Outline.ttf
115 | | + Mime type: application/x-truetype-font
116 | | + File data, size: 33233
117 | | + File UID: 186099121
118 | | + Attached
119 | | + File name: ariah_.ttf
120 | | + Mime type: application/x-truetype-font
121 | | + File data, size: 181020
122 | | + File UID: 912525466
123 | | + Attached
124 | | + File name: arial.ttf
125 | | + Mime type: application/x-truetype-font
126 | | + File data, size: 766656
127 | | + File UID: 1718635149
128 | | + Attached
129 | | + File name: Arialic Hollow.ttf
130 | | + Mime type: application/x-truetype-font
131 | | + File data, size: 181020
132 | | + File UID: 1909604002
133 | | + Attached
134 | | + File name: ARLRDBD.TTF
135 | | + Mime type: application/x-truetype-font
136 | | + File data, size: 45260
137 | | + File UID: 64785322
138 | | + Attached
139 | | + File name: AUBREY1__.TTF
140 | | + Mime type: application/x-truetype-font
141 | | + File data, size: 26872
142 | | + File UID: 510261437
143 | | + Attached
144 | | + File name: CatShop.ttf
145 | | + Mime type: application/x-truetype-font
146 | | + File data, size: 79588
147 | | + File UID: 2756332423
148 | | + Attached
149 | | + File name: CENTURY.TTF
150 | | + Mime type: application/x-truetype-font
151 | | + File data, size: 165248
152 | | + File UID: 593692811
153 | | + Attached
154 | | + File name: CENTURYO.TTF
155 | | + Mime type: application/x-truetype-font
156 | | + File data, size: 38128
157 | | + File UID: 3101324205
158 | | + Attached
159 | | + File name: Coprgtb.TTF
160 | | + Mime type: application/x-truetype-font
161 | | + File data, size: 61552
162 | | + File UID: 3011818924
163 | | + Attached
164 | | + File name: DanteMTStd-Bold.otf
165 | | + Mime type: application/x-truetype-font
166 | | + File data, size: 54476
167 | | + File UID: 2857265596
168 | | + Attached
169 | | + File name: Disney_Simple.ttf
170 | | + Mime type: application/x-truetype-font
171 | | + File data, size: 54908
172 | | + File UID: 380671634
173 | | + Attached
174 | | + File name: edosz.ttf
175 | | + Mime type: application/x-truetype-font
176 | | + File data, size: 48820
177 | | + File UID: 1146176129
178 | | + Attached
179 | | + File name: Fansub Block-Ozaki.ttf
180 | | + Mime type: application/x-truetype-font
181 | | + File data, size: 1812
182 | | + File UID: 4251068810
183 | | + Attached
184 | | + File name: fansubBlock_0.ttf
185 | | + Mime type: application/x-truetype-font
186 | | + File data, size: 3364
187 | | + File UID: 3748357566
188 | | + Attached
189 | | + File name: FNT_BS.TTF
190 | | + Mime type: application/x-truetype-font
191 | | + File data, size: 20988
192 | | + File UID: 2420582037
193 | | + Attached
194 | | + File name: georgia.ttf
195 | | + Mime type: application/x-truetype-font
196 | | + File data, size: 157080
197 | | + File UID: 391120276
198 | | + Attached
199 | | + File name: GillSansStd.otf
200 | | + Mime type: application/x-truetype-font
201 | | + File data, size: 28880
202 | | + File UID: 969964701
203 | | + Attached
204 | | + File name: GillSansStd-Bold.otf
205 | | + Mime type: application/x-truetype-font
206 | | + File data, size: 29668
207 | | + File UID: 1400380071
208 | | + Attached
209 | | + File name: hongkong.ttf
210 | | + Mime type: application/x-truetype-font
211 | | + File data, size: 43544
212 | | + File UID: 712839100
213 | | + Attached
214 | | + File name: IwaOMinPro-Bd-Fate.ttf
215 | | + Mime type: application/x-truetype-font
216 | | + File data, size: 700900
217 | | + File UID: 1824292757
218 | | + Attached
219 | | + File name: JFRocSol.TTF
220 | | + Mime type: application/x-truetype-font
221 | | + File data, size: 50284
222 | | + File UID: 1671813507
223 | | + Attached
224 | | + File name: jsa_lovechinese.ttf
225 | | + Mime type: application/x-truetype-font
226 | | + File data, size: 34820
227 | | + File UID: 1847369877
228 | | + Attached
229 | | + File name: KGFallForYou.ttf
230 | | + Mime type: application/x-truetype-font
231 | | + File data, size: 29724
232 | | + File UID: 1050574230
233 | | + Attached
234 | | + File name: KIRBY-H.TTF
235 | | + Mime type: application/x-truetype-font
236 | | + File data, size: 49888
237 | | + File UID: 4280517808
238 | | + Attached
239 | | + File name: lightmorning.ttf
240 | | + Mime type: application/x-truetype-font
241 | | + File data, size: 23516
242 | | + File UID: 2164373178
243 | | + Attached
244 | | + File name: mangat.ttf
245 | | + Mime type: application/x-truetype-font
246 | | + File data, size: 29964
247 | | + File UID: 278465691
248 | | + Attached
249 | | + File name: MLSGU.TTF
250 | | + Mime type: application/x-truetype-font
251 | | + File data, size: 69555
252 | | + File UID: 755965107
253 | | + Attached
254 | | + File name: MyriadPro-Bold.otf
255 | | + Mime type: application/x-truetype-font
256 | | + File data, size: 81436
257 | | + File UID: 616126625
258 | | + Attached
259 | | + File name: oakwood.ttf
260 | | + Mime type: application/x-truetype-font
261 | | + File data, size: 45884
262 | | + File UID: 2952023949
263 | | + Attached
264 | | + File name: Old_Rubber_Stamp.ttf
265 | | + Mime type: application/x-truetype-font
266 | | + File data, size: 42436
267 | | + File UID: 2372587694
268 | | + Attached
269 | | + File name: pastel crayon.ttf
270 | | + Mime type: application/x-truetype-font
271 | | + File data, size: 303164
272 | | + File UID: 741350831
273 | | + Attached
274 | | + File name: phillysansps.otf
275 | | + Mime type: application/x-truetype-font
276 | | + File data, size: 5064
277 | | + File UID: 1617570210
278 | | + Attached
279 | | + File name: pixelmix.ttf
280 | | + Mime type: application/x-truetype-font
281 | | + File data, size: 30232
282 | | + File UID: 872785811
283 | | + Attached
284 | | + File name: Plane Crash.ttf
285 | | + Mime type: application/x-truetype-font
286 | | + File data, size: 286512
287 | | + File UID: 3273697459
288 | | + Attached
289 | | + File name: SEVEMFBR.TTF
290 | | + Mime type: application/x-truetype-font
291 | | + File data, size: 23904
292 | | + File UID: 4132532117
293 | |+ Chapters
294 | | + EditionEntry
295 | | + EditionFlagOrdered: 1
296 | | + EditionFlagHidden: 0
297 | | + EditionFlagDefault: 1
298 | | + EditionUID: 2906622092
299 | | + ChapterAtom
300 | | + ChapterUID: 3143058099
301 | | + ChapterTimeStart: 00:00:00.000000000
302 | | + ChapterTimeEnd: 00:05:56.982000000
303 | | + ChapterFlagHidden: 0
304 | | + ChapterFlagEnabled: 1
305 | | + ChapterDisplay
306 | | + ChapterString: Prologue
307 | | + ChapterLanguage: eng
308 | | + ChapterAtom
309 | | + ChapterUID: 3143058098
310 | | + ChapterTimeStart: 00:00:00.000000000
311 | | + ChapterTimeEnd: 00:01:30.048000000
312 | | + ChapterFlagHidden: 0
313 | | + ChapterFlagEnabled: 1
314 | | + ChapterSegmentUID: length 16, data: 0x88 0x79 0x26 0x47 0x4c 0x7d 0x6a 0x4a 0x98 0x4d 0x93 0x8a 0xe8 0xe0 0x11 0x92
315 | | + ChapterDisplay
316 | | + ChapterString: Opening
317 | | + ChapterLanguage: eng
318 | | + ChapterAtom
319 | | + ChapterUID: 3143058097
320 | | + ChapterTimeStart: 00:05:57.019000000
321 | | + ChapterTimeEnd: 00:22:40.025000000
322 | | + ChapterFlagHidden: 0
323 | | + ChapterFlagEnabled: 1
324 | | + ChapterDisplay
325 | | + ChapterString: Episode
326 | | + ChapterLanguage: eng
327 | | + ChapterAtom
328 | | + ChapterUID: 3143058096
329 | | + ChapterTimeStart: 00:00:00.000000000
330 | | + ChapterTimeEnd: 00:01:30.048000000
331 | | + ChapterFlagHidden: 0
332 | | + ChapterFlagEnabled: 1
333 | | + ChapterSegmentUID: length 16, data: 0xb4 0x6d 0x8b 0x99 0xe1 0x6b 0x17 0x36 0xae 0xe4 0x12 0x1f 0x8a 0x34 0x82 0x79
334 | | + ChapterDisplay
335 | | + ChapterString: Ending
336 | | + ChapterLanguage: eng
337 | | + ChapterAtom
338 | | + ChapterUID: 3143058095
339 | | + ChapterTimeStart: 00:22:40.059000000
340 | | + ChapterTimeEnd: 00:23:15.060000000
341 | | + ChapterFlagHidden: 0
342 | | + ChapterFlagEnabled: 1
343 | | + ChapterDisplay
344 | | + ChapterString: Preview
345 | | + ChapterLanguage: eng
346 | |+ EbmlVoid (size: 101)
347 | |+ Cluster
348 | ";
349 | var document = mapper.DecodeStringIntoDocument(data);
350 | this._helper.WriteLine(document.ToString());
351 |
352 | var info = new MkvInfo(document);
353 |
354 |
355 | // TODO: There's some duplication here as a result of some of the attributes
356 | // It won't really affect what I want to do for the application but it might into the future affect
357 | // something else as well..
358 | this._helper.WriteLine("!");
359 | }
360 |
361 | }
362 | }
--------------------------------------------------------------------------------
/UnlinkMKV-GUI/UnlinkMKV-GUI/winport.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env perl
2 | # UnlinkMKV - Undo segment linking in MKV files
3 | # Garret Noling 2013-2015
4 |
5 | require 5.010;
6 | use strict;
7 | use XML::LibXML;
8 | use File::Glob qw/:globally :nocase/;
9 | use Math::BigFloat qw/:constant/;
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 | GetOptions (
22 | 'll|loglevel=s' => \$loglevel,
23 | );
24 | my $conf = qq(
25 | log4perl.logger = $loglevel, STDINF
26 | log4perl.appender.STDINF = Log::Log4perl::Appender::Screen
27 | log4perl.appender.STDINF.stderr = 0
28 | log4perl.appender.STDINF.layout = PatternLayout
29 | log4perl.appender.STDINF.layout.ConversionPattern = %x%m{chomp}%n
30 | );
31 | Log::Log4perl->init_once(\$conf);
32 | Log::Log4perl::NDC->push("");
33 | INFO "UnlinkMKV";
34 | UnlinkMKV::more();
35 |
36 | my $opt;
37 | my $basedir = canonpath(dirname(abs_path($0)));
38 | my $inifile = catfile($basedir, "unlinkmkv.ini");
39 | my $cwd = canonpath(cwd());
40 | $opt->{out_dir} = canonpath("$cwd/UMKV");
41 | $opt->{tmpdir} = canonpath("$cwd/UMKV/tmp");
42 | $opt->{ffmpeg} = "ffmpeg";
43 | $opt->{mkvext} = "mkvextract";
44 | $opt->{mkvinfo} = "mkvinfo";
45 | $opt->{mkvmerge} = "mkvmerge";
46 | $opt->{fixaudio} = 0;
47 | $opt->{fixvideo} = 0;
48 | $opt->{fixsubtitles} = 1;
49 | $opt->{ignoredefaultflag} = 0;
50 | $opt->{chapters} = 1;
51 | $opt->{playresx};
52 | $opt->{playresy};
53 |
54 | if(-f $inifile) {
55 | open my $F, $inifile or LOGDIE "failed to open unlinkmkv.ini: $!";
56 | while(my $line = <$F>) {
57 | chomp($line);
58 | $line =~ /^[\s\t]*([a-z0-9_]+)[\s\t]*=[\s\t]*["']?([^\s\t].*[^\s\t]?)["']?[\s\t]*$/;
59 | my $key = $1;
60 | my $val = $2;
61 | $val =~ s/\$basedir/$basedir/g;
62 | DEBUG "[ini] [$key] = [$val]";
63 | $opt->{$key} = $val;
64 | }
65 | close $F;
66 | }
67 |
68 | GetOptions ( \%$opt,
69 | 'tmpdir=s',
70 | 'fixaudio|fa!',
71 | 'fixvideo|fv!',
72 | 'fixsubtitles|fs!',
73 | 'outdir=s',
74 | 'playresx=i',
75 | 'playresy=i',
76 | 'ignoredefaultflag',
77 | 'chapters!',
78 | 'ffmpeg=s',
79 | 'mkvextract=s',
80 | 'mkvinfo=s',
81 | 'mkvmerge=s',
82 | );
83 |
84 | $opt->{out_dir} = canonpath($opt->{out_dir});
85 | $opt->{ffmpeg} = canonpath(abs_path(which $opt->{ffmpeg}));
86 | $opt->{mkvext} = canonpath(abs_path(which $opt->{mkvext}));
87 | $opt->{mkvinfo} = canonpath(abs_path(which $opt->{mkvinfo}));
88 | $opt->{mkvmerge} = canonpath(abs_path(which $opt->{mkvmerge}));
89 | $opt->{tmpdir} = canonpath($opt->{tmpdir});
90 |
91 | UnlinkMKV::more();
92 | INFO "Options";
93 | UnlinkMKV::more();
94 | foreach my $key (sort keys %$opt) {
95 | INFO "$key: $opt->{$key}";
96 | }
97 | UnlinkMKV::less();
98 | UnlinkMKV::less();
99 |
100 | my $UMKV = UnlinkMKV->new({
101 | ffmpeg => $opt->{ffmpeg},
102 | mkvext => $opt->{mkvext},
103 | mkvinfo => $opt->{mkvinfo},
104 | mkvmerge => $opt->{mkvmerge},
105 | tmp => $opt->{tmpdir},
106 | fixaudio => $opt->{fixaudio},
107 | fixvideo => $opt->{fixvideo},
108 | fixsubs => $opt->{fixsubtitles},
109 | outdir => $opt->{out_dir},
110 | playresx => $opt->{playresx},
111 | playresy => $opt->{playresy},
112 | ignoredefaultflag => $opt->{ignoredefaultflag},
113 | chapters => $opt->{chapters},
114 | });
115 |
116 | if(scalar(@ARGV) == 0) {
117 | push @ARGV, $cwd;
118 | }
119 |
120 | my @LIST;
121 | foreach my $item (@ARGV) {
122 | if(-d $item) {
123 | opendir my $D, $item;
124 | while (my $F = readdir($D)) {
125 | if (-f catfile($item, $F) && $F =~ /\.mkv$/i && !-f catfile($opt->{out_dir}, $F)) {
126 | push @LIST, canonpath(abs_path("$item/$F"));
127 | }
128 | }
129 | closedir $D;
130 | }
131 | elsif(-f $item) {
132 | push @LIST, canonpath(abs_path($item));
133 | }
134 | }
135 | do { $UMKV->process($_) } for @LIST;
136 | exit;
137 |
138 | package UnlinkMKV {
139 | use strict;
140 | use XML::LibXML;
141 | use File::Glob qw/:globally :nocase/;
142 | use Math::BigFloat qw/:constant/;
143 | use Getopt::Long qw/:config passthrough/;
144 | use Log::Log4perl qw/:easy/;
145 | use File::Basename;
146 | use String::CRC32;
147 | use IPC::Run3;
148 | use Cwd qw/cwd realpath abs_path/;
149 | use File::Path qw/make_path rmtree/;
150 | use File::Copy;
151 | use File::Spec::Functions;
152 |
153 | sub new {
154 | my $type = shift;
155 | my $opt = shift;
156 | my ($self) = {};
157 | bless($self, $type);
158 | $self->{opt} = $opt;
159 | $self->{xml} = XML::LibXML->new();
160 | $self->mktmp();
161 | return $self;
162 | }
163 |
164 | sub DESTROY {
165 | my $self = shift;
166 | if(-d $self->{tmp}) {
167 | chdir($self->{opt}->{outdir});
168 | rmtree($self->{tmp}, 0, 1);
169 | DEBUG "removed tmp $self->{tmp}";
170 | }
171 | DEBUG "exiting";
172 | }
173 |
174 | sub mktmp {
175 | my $self = shift;
176 | if(!defined $self->{opt}->{tmp}) {
177 | $self->{tmp} = canonpath(cwd()."/UnlinkMKV/tmp/$$");
178 | }
179 | else {
180 | $self->{tmp} = catfile($self->{opt}->{tmp}, $$);
181 | }
182 | if(-d $self->{tmp}) {
183 | rmtree($self->{tmp}, 0, 1);
184 | }
185 | DEBUG "removed tmp $self->{tmp}";
186 | $self->{attachdir} = catfile($self->{tmp}, 'attach');
187 | $self->{partsdir} = catfile($self->{tmp}, 'parts');
188 | $self->{encodesdir} = catfile($self->{tmp}, 'encodes');
189 | $self->{subtitlesdir} = catfile($self->{tmp}, 'subtitles');
190 | $self->{segmentsdir} = catfile($self->{tmp}, 'segments');
191 | make_path(
192 | $self->{tmp},
193 | $self->{attachdir},
194 | $self->{partsdir},
195 | $self->{encodesdir},
196 | $self->{subtitlesdir},
197 | $self->{segmentsdir},
198 | , { verbose => 0 });
199 | DEBUG "created tmp $self->{tmp}";
200 | }
201 |
202 | sub process {
203 | my $self = shift;
204 | my $item = shift;
205 | my $origpath = canonpath(dirname($item));
206 | chdir($origpath);
207 | INFO "processing $item";
208 | more();
209 | INFO "checking if file is segmented";
210 | if($self->is_linked($item)) {
211 | INFO "generating chapter file";
212 | more();
213 | my(@segments, @splits);
214 | my($parent, $dir, $suffix) = fileparse($item, qr/\.[mM][kK][vV]/);
215 | INFO "loading chapters";
216 | more();
217 | my $xml = $self->{xml}->load_xml(string => $self->sys($self->{opt}->{mkvext}, 'chapters', $item));
218 | open my $out_chapters_orig, '>', catfile($self->{tmp}, "$parent-chapters-original.xml");
219 | print {$out_chapters_orig} $xml->toString;
220 | close $out_chapters_orig;
221 | my $offs_time_end = '00:00:00.000000000';
222 | my $last_time_end = '00:00:00.000000000';
223 | my $offset = '00:00:00.000000000';
224 | my $chaptercount = 1;
225 | foreach my $edition ($xml->findnodes('//EditionFlagDefault[.=0]')) {
226 | if(!$self->{opt}->{ignoredefaultflag}) {
227 | $edition->parentNode->unbindNode;
228 | WARN "non-default chapter dropped";
229 | }
230 | else {
231 | INFO "non-default chapter kept on purpose";
232 | }
233 | }
234 | foreach my $chapter ($xml->findnodes('//ChapterAtom')) {
235 | my ($ChapterTimeStart) = $chapter->findnodes('./ChapterTimeStart/text()');
236 | my ($ChapterTimeEnd) = $chapter->findnodes('./ChapterTimeEnd/text()');
237 | my $ChapterEnabled = ($chapter->findvalue('ChapterFlagEnabled') =~ /^\d+$/) ? $chapter->findvalue('ChapterFlagEnabled') : 1;
238 | if($chapter->exists('ChapterSegmentUID') && $ChapterEnabled) {
239 | my ($SegmentUID, $SegmentELE, $SegmentUIDText);
240 | ($SegmentELE) = $chapter->findnodes('./ChapterSegmentUID');
241 | ($SegmentUID) = $chapter->findnodes('./ChapterSegmentUID/text()');
242 | $SegmentUIDText = $SegmentUID->textContent();
243 | if($SegmentELE->getAttribute('format') eq 'hex') {
244 | $SegmentUIDText =~ s/\n//g;
245 | $SegmentUIDText =~ s/\s//g;
246 | $SegmentUIDText =~ s/([a-zA-Z0-9]{2})/ 0x$1/g;
247 | $SegmentUIDText =~ s/^\s//;
248 | }
249 | elsif($SegmentELE->getAttribute('format') eq 'ascii') {
250 | $SegmentUIDText =~ s/(.)/sprintf("0x%x ",ord($1))/eg;
251 | $SegmentUIDText =~ s/\s$//;
252 | }
253 | push @segments, {
254 | start => $ChapterTimeStart->textContent(),
255 | stop => $ChapterTimeEnd->textContent(),
256 | id => $SegmentUIDText,
257 | split_start => $last_time_end
258 | };
259 | push @splits, $last_time_end unless $last_time_end eq '00:00:00.000000000';
260 | $offset = $self->add_duration_to_timecode($offset, $ChapterTimeEnd->textContent());
261 | if($offs_time_end eq '00:00:00.000000000' && $chaptercount > 1) {
262 | $ChapterTimeStart->setData($offset);
263 | $ChapterTimeEnd->setData($self->add_duration_to_timecode($offset, $ChapterTimeEnd->textContent()));
264 | }
265 | else {
266 | $ChapterTimeStart->setData($offs_time_end);
267 | $ChapterTimeEnd->setData($self->add_duration_to_timecode($offs_time_end, $ChapterTimeEnd->textContent()));
268 | }
269 | $offs_time_end = $ChapterTimeEnd->textContent();
270 | $chapter->removeChild($chapter->findnodes('./ChapterSegmentUID'));
271 | INFO "external";
272 | more();
273 | INFO "chstart " . $ChapterTimeStart->textContent();
274 | INFO "chend " . $ChapterTimeEnd->textContent();
275 | INFO "offset " . $offset;
276 | INFO "offte " . $offs_time_end;
277 | INFO "chen " . $ChapterEnabled;
278 | less();
279 | }
280 | else {
281 | eval { if(defined $ChapterTimeEnd->textContent() && defined $ChapterTimeStart->textContent()){}; 1 } or next;
282 | push @segments, {
283 | file => $self->setpart(basename($item), abs_path($item)),
284 | start => $ChapterTimeStart->textContent(),
285 | stop => $ChapterTimeEnd->textContent(),
286 | split_start => $ChapterTimeStart->textContent(),
287 | split_stop => $ChapterTimeEnd->textContent()
288 | };
289 | $last_time_end = $ChapterTimeEnd->textContent();
290 | $ChapterTimeStart->setData($self->add_duration_to_timecode($ChapterTimeStart->textContent(), $offset));
291 | $ChapterTimeEnd->setData($self->add_duration_to_timecode($ChapterTimeEnd->textContent(), $offset));
292 | $offs_time_end = $ChapterTimeEnd->textContent();
293 | INFO "internal";
294 | more();
295 | INFO "chstart " . $ChapterTimeStart->textContent();
296 | INFO "chend " . $ChapterTimeEnd->textContent();
297 | INFO "offset " . $offset;
298 | INFO "offte " . $offs_time_end;
299 | INFO "chen " . $ChapterEnabled;
300 | less();
301 | }
302 | $chaptercount++;
303 | }
304 | less();
305 |
306 | INFO "writing chapter temporary file";
307 | open my $out_chapters, '>', catfile($self->{tmp}, "$parent-chapters.xml");
308 | print {$out_chapters} $xml->toString;
309 | close $out_chapters;
310 |
311 | INFO "looking for segment parts";
312 | more();
313 | foreach my $mkv (<*.mkv>) {
314 | next if $mkv eq $item;
315 | $mkv = canonpath(abs_path(catfile($dir, $mkv)));
316 | my ($id, $dur) = $self->mkvinfo($mkv);
317 | for (@segments) {
318 | next unless defined $_->{id};
319 | if($_->{id} eq $id && basename($mkv) ne basename($item)) {
320 | $_->{file} = $self->setpart(basename($mkv), $mkv);
321 | INFO "found part $_->{file}";
322 | }
323 | }
324 | }
325 | less();
326 |
327 | INFO "checking that all required segments were found";
328 | my $okay_to_proceed = 1;
329 | for (@segments) {
330 | if(defined $_->{id} && !defined $_->{file}) {
331 | DEBUG "missing segment: $_->{id}";
332 | $okay_to_proceed = 0;
333 | }
334 | }
335 | if(!$okay_to_proceed) {
336 | WARN "missing segments!";
337 | return;
338 | }
339 |
340 | INFO "extracting attachments";
341 | more();
342 | foreach my $seg (@segments) {
343 | my $file = "$seg->{file}";
344 | my $in = 0;
345 | my ($N, $T, $D, $U);
346 | TRACE "FILE $file";
347 | for(split /\n/, $self->sys($self->{opt}->{mkvinfo}, $file)) {
348 | chomp;
349 | if ($_ =~ /^\| \+ Attached/) {
350 | $in = 1;
351 | }
352 | elsif($in && $_ =~ /File name: (.*)/) {
353 | $N = $1;
354 | }
355 | elsif($in && $_ =~ /Mime type: (.*)/) {
356 | $T = $1;
357 | }
358 | elsif($in && $_ =~ /File data, size: (.*)/) {
359 | $D = $1;
360 | }
361 | elsif($in && $_ =~ /File UID: (.*)/) {
362 | $U = $1;
363 | }
364 | if (defined $N && defined $T && defined $D && defined $U) {
365 | push @{$seg->{attachments}}, { name => $N, type => $T, data => $D, UID => $U };
366 | undef $N;
367 | undef $T;
368 | undef $D;
369 | undef $U;
370 | }
371 | }
372 | if (defined $seg->{attachments} && @{$seg->{attachments}} > 0) {
373 | my $dir = cwd();
374 | TRACE "chdir $self->{attachdir}";
375 | chdir($self->{attachdir});
376 | $self->sys($self->{opt}->{mkvext}, 'attachments', $file, (1..$#{$seg->{attachments}}+1));
377 | chdir($dir);
378 | }
379 | }
380 | less();
381 |
382 | my @atts;
383 | opendir my $D, $self->{attachdir} or LOGDIE "failed to open attachment directory: $!";
384 | while(my $item = readdir($D)) {
385 | my $F = catfile($self->{attachdir}, $item);
386 | if(-f $F) {
387 | push @atts, ('--attachment-mime-type', 'application/x-truetype-font', '--attach-file', $F);
388 | }
389 | }
390 | closedir $D;
391 |
392 | INFO "creating splits";
393 | more();
394 | if(scalar(@splits) > 0) {
395 | DEBUG "splitting file: $item";
396 | $self->sys($self->{opt}->{mkvmerge}, '--no-chapters', '-o', catfile($self->{partsdir}, "split-%03d.mkv"), $item, '--split', 'timecodes:' . join(',',@splits));
397 | }
398 | less();
399 |
400 | INFO "setting parts";
401 | more();
402 | my (@parts, $LAST);
403 | my $count = 1;
404 | foreach my $segment (@segments) {
405 | if(defined $segment->{id} && $segment->{start} =~ /^00:00:00\./ || ($LAST ne $segment->{file} && scalar(@splits) == 0)) {
406 | DEBUG "part $segment->{file}";
407 | push @parts, $segment->{file};
408 | }
409 | elsif($LAST ne $segment->{file}) {
410 | my $f = catfile($self->{partsdir}, sprintf("split-%03d.mkv",$count));
411 | DEBUG "part $f";
412 | push @parts, $f;
413 | $count++;
414 | }
415 | $LAST = $segment->{file};
416 | }
417 | less();
418 |
419 | my $subs;
420 | if($self->{opt}->{fixsubs}) {
421 | INFO "extracting subs";
422 | more();
423 | foreach my $part (@parts) {
424 | DEBUG "$part";
425 | my $in = 0;
426 | my $sub = 0;
427 | my ($N, $T, $D, $U);
428 | for (split /\n/, $self->sys($self->{opt}->{mkvinfo}, $part)) {
429 | chomp;
430 | if ($_ =~ /^\| \+ A track/) {
431 | $in = 1;
432 | undef $N;
433 | undef $T;
434 | undef $D;
435 | undef $U;
436 | $sub = 0;
437 | }
438 | elsif($in && $_ =~ /Track type: subtitles/) {
439 | $sub = 1;
440 | }
441 | elsif($in && $_ =~ /Track number: .*: (\d)\)$/) {
442 | $T = $1;
443 | }
444 | if (defined $in && $sub && $T) {
445 | my $sf = catfile($self->{subtitlesdir}, basename($part)."-$T.ass");
446 | $self->sys($self->{opt}->{mkvext}, 'tracks', $part, "$T:$sf");
447 | push @{$subs->{$part}}, $sf;
448 | undef $T;
449 | $in = 0;
450 | $sub = 0;
451 | }
452 | }
453 | }
454 | less();
455 |
456 | INFO "making substyles unique";
457 | more();
458 | my $styles;
459 | foreach my $f (keys %$subs) {
460 | push @$styles, @{$self->uniquify_substyles($subs->{$f})};
461 | }
462 | less();
463 |
464 | INFO "mashing unique substyles to all parts";
465 | more();
466 | foreach my $f (keys %$subs) {
467 | $self->mush_substyles($subs->{$f}, $styles);
468 | }
469 | less();
470 |
471 | INFO "remuxing subtitles";
472 | more();
473 | foreach my $f (keys %$subs) {
474 | DEBUG $f;
475 | my @stracks;
476 | foreach my $T (@{$subs->{$f}}) {
477 | push @stracks, $T;
478 | }
479 | $self->sys($self->{opt}->{mkvmerge}, '-o', "$f-fixsubs.mkv", '--no-chapters', '--no-subs', $f, @stracks, @atts);
480 | $self->replace($f, "$f-fixsubs.mkv");
481 | }
482 | less();
483 | }
484 |
485 | if($self->{opt}->{fixvideo} || $self->{opt}->{fixaudio}) {
486 | INFO "encoding parts";
487 | more();
488 | foreach my $part (@parts) {
489 | my @vopt = qw/-vcodec copy/;
490 | my @aopt = qw/-map 0 -acodec copy/;
491 | WARN $part;
492 | if ($self->{opt}->{fixvideo}) {
493 | my $br = $self->bitrate($part);
494 | @vopt = undef;
495 | @vopt = ('-c:v', 'libx264', '-b:v', $br.'k', '-minrate', $br.'k', '-maxrate', ($br*2).'k', '-bufsize', '1835k');
496 | }
497 | if ($self->{opt}->{fixaudio}) {
498 | @aopt = undef;
499 | @aopt = qw/-map 0 -acodec ac3 -ab 320k/;
500 | }
501 | $self->sys($self->{opt}->{ffmpeg}, '-i', $part, @vopt, @aopt, "$part-fixed.mkv");
502 | $self->replace($part, "$part-fixed.mkv");
503 | }
504 | less();
505 | }
506 |
507 | INFO "building file";
508 | more();
509 | my @PRTS;
510 | foreach my $part (@parts) {
511 | push @PRTS, $part;
512 | push @PRTS, '+';
513 | }
514 | pop @PRTS;
515 | if($self->{opt}->{chapters}) {
516 | $self->sys($self->{opt}->{mkvmerge}, '--no-chapters', '-M', '--chapters', catfile($self->{tmp}, "$parent-chapters.xml"), '-o', catfile($self->{encodesdir}, basename($item)), @PRTS);
517 | }
518 | else {
519 | $self->sys($self->{opt}->{mkvmerge}, '--no-chapters', '-M', '--chapters', catfile($self->{tmp}, "$parent-chapters.xml"), '-o', catfile($self->{encodesdir}, basename($item)), @PRTS);
520 | }
521 | less();
522 |
523 | INFO "fixing subs, again... (maybe an mkvmerge issue?)";
524 | more();
525 | if($self->{opt}->{fixsubs}) {
526 | my @FS;
527 | my $in = 0;
528 | my $sub = 0;
529 | my ($N, $T, $D, $U);
530 | for (split /\n/, $self->sys($self->{opt}->{mkvinfo}, catfile($self->{encodesdir}, basename($item)))) {
531 | chomp;
532 | if ($_ =~ /^\| \+ A track/) {
533 | $in = 1;
534 | undef $N;
535 | undef $T;
536 | undef $D;
537 | undef $U;
538 | $sub = 0;
539 | }
540 | elsif($in && $_ =~ /Track type: subtitles/) {
541 | $sub = 1;
542 | }
543 | elsif($in && $_ =~ /Track number: .*: (\d)\)$/) {
544 | $T = $1;
545 | }
546 | if (defined $in && $sub && $T) {
547 | $self->sys($self->{opt}->{mkvext}, 'tracks', catfile($self->{encodesdir}, basename($item)), "$T:".catfile($self->{encodesdir}, "$T.ass"));
548 | push @FS, catfile($self->{encodesdir}, "$T.ass");
549 | undef $T;
550 | $in = 0;
551 | $sub = 0;
552 | }
553 | }
554 | $self->sys($self->{opt}->{mkvmerge}, '-o', catfile($self->{encodesdir}, "fixed." . basename($item)), '-S', catfile($self->{encodesdir}, basename($item)), @FS);
555 | $self->replace(catfile($self->{encodesdir}, basename($item)), catfile($self->{encodesdir}, "fixed." . basename($item)));
556 | }
557 | less();
558 |
559 | INFO "moving built file to final destination";
560 | more();
561 | make_path($self->{opt}->{outdir}, { verbose => 0 });
562 | move(catfile($self->{encodesdir}, basename($item)), $self->{opt}->{outdir});
563 | less();
564 | }
565 | $self->mktmp();
566 | less();
567 | }
568 |
569 | sub bitrate {
570 | my $self = shift;
571 | my $file = shift;
572 | my $size = int(((-s $file)/1024+.5)*1.1);
573 | my $br = 2000;
574 | foreach my $line (split /\n/, $self->sys($self->{opt}->{ffmpeg}, '-i', $file)) {
575 | if($line =~ /duration: (\d+):(\d+):(\d+\.\d+),/i) {
576 | my $duration = ($1*3600)+($2*60)+int($3+.5);
577 | $br = int(($size / $duration)+.5);
578 | DEBUG "duration [$1:$2:$3] = $duration seconds, fsize $size = ${br}k bitrate";
579 | }
580 | if($line =~ /duration.*bitrate: (\d+) k/i) {
581 | $br = $1;
582 | DEBUG "original bitrate $br";
583 | }
584 | }
585 | return $br;
586 | }
587 |
588 | sub replace {
589 | my $self = shift;
590 | my $dest = shift;
591 | my $source = shift;
592 | unlink($dest);
593 | move($source, $dest);
594 | }
595 |
596 | sub is_linked {
597 | my $self = shift;
598 | my $item = shift;
599 | my $linked = 0;
600 | more();
601 | foreach my $line (split /\n/, $self->sys($self->{opt}->{mkvext}, 'chapters', $item)) {
602 | if($line =~ /{partsdir}, $link);
621 | DEBUG "copying part $file to $part";
622 | copy($file, $part);
623 | return $part;
624 | }
625 |
626 | sub uniquify_substyles {
627 | my $self = shift;
628 | my $S = shift;
629 | my @styles;
630 | foreach my $T (@$S) {
631 | DEBUG $T;
632 | my $uniq = crc32($T);
633 | open my $O, '>', "$T.new";
634 | open my $F, '<', $T;
635 | my $in = 0;
636 | my $di = 0;
637 | my $key;
638 | while(my $line = <$F>) {
639 | if ($line =~ /^\[/ && $line =~ /^\[V4\+ Styles/) {
640 | $in = 1;
641 | }
642 | elsif(($in||$di) && !defined $key && $line =~ /^Format:/i) {
643 | my $test = "$line";
644 | $test =~ s/ //g;
645 | $test =~ s/^format://i;
646 | my(@parts) = split /,/, $test;
647 | my $c = 0;
648 | foreach my $part (@parts) {
649 | if ($in && $part =~ /^name$/i) {
650 | $key = $c;
651 | }
652 | elsif($di && $part =~ /^style$/i) {
653 | $key = $c;
654 | }
655 | $c++;
656 | }
657 | }
658 | elsif($in && defined $key && $line =~ /^style:/i) {
659 | $line =~ s/^style:\s+?//i;
660 | my(@parts) = split /,/, $line;
661 | $parts[$key] = "$parts[$key] u$uniq";
662 | $line = "Style: " . join(',', @parts);
663 | push @styles, $line;
664 | DEBUG $line;
665 | }
666 | elsif($line =~ /^\[Events/i) {
667 | $in = 0;
668 | $di = 1;
669 | $key = undef;
670 | }
671 | elsif($di && defined $key && $line =~ /^dialogue:/i) {
672 | $line =~ s/^dialogue: //i;
673 | my(@parts) = split /,/, $line;
674 | $parts[$key] = "$parts[$key] u$uniq";
675 | $line = "Dialogue: " . join(',', @parts);
676 | }
677 | print $O $line;
678 | }
679 | close $F;
680 | close $O;
681 | unlink $T;
682 | move("$T.new", $T);
683 | }
684 | return \@styles;
685 | }
686 |
687 | sub mush_substyles {
688 | my $self = shift;
689 | my $S = shift;
690 | my $styles = shift;
691 | foreach my $T (@$S) {
692 | open my $F, '<', $T;
693 | my @lines = <$F>;
694 | close $F;
695 | open my $F, '>', $T;
696 | my $in = 0;
697 | foreach my $line (@lines) {
698 | if ($line =~ /^\[/ && $line =~ /^\[V4\+ Styles/) {
699 | $in = 1;
700 | print $F $line;
701 | }
702 | elsif($in && $line =~ /^format:/i) {
703 | print $F $line;
704 | do {
705 | print $F $_;
706 | } for @$styles;
707 | }
708 | elsif($in && $line =~ /^style:/i) {
709 | #do nothing
710 | }
711 | elsif($in && $line =~ /^\[/) {
712 | $in = 0;
713 | print $F $line;
714 | }
715 | elsif(defined $self->{opt}->{playresx} && $line =~ /^PlayResX:/) {
716 | print $F "PlayResX: $self->{opt}->{playresx}\n";
717 | }
718 | elsif(defined $self->{opt}->{playresy} && $line =~ /^PlayResY:/) {
719 | print $F "PlayResY: $self->{opt}->{playresy}\n";
720 | }
721 | else {
722 | print $F $line;
723 | }
724 | }
725 | close $F;
726 | }
727 | }
728 |
729 | sub mkvinfo {
730 | my $self = shift;
731 | my $file = shift;
732 | my $id = '';
733 | my $dur = '';
734 | for(split /\n/, $self->sys($self->{opt}->{mkvinfo}, $file)) {
735 | chomp;
736 | if ($_ =~ /Segment[ \-]UID:/) {
737 | $_ =~ /Segment[ \-]UID:([a-zA-Z0-9\s]+)$/;
738 | $id = $1;
739 | $id =~ s/^\s+//g;
740 | $id =~ s/\s+$//g;
741 | }
742 | elsif($_ =~ /^\| \+ Duration:/) {
743 | $_ =~ /^\| \+ Duration:.*\((.*)\)/;
744 | $dur = $1;
745 | }
746 | }
747 | return ($id, $dur);
748 | }
749 |
750 | sub add_duration_to_timecode {
751 | my $self = shift;
752 | my $time = shift;
753 | my $dur = shift;
754 | my ($th, $tm, $ts) = split /:/, $time;
755 | my ($dh, $dm, $ds) = split /:/, $dur;
756 | $ts += ($ds + 0.000000001);
757 | if($ts >= 60.000000000) {
758 | $ts = $ts - 60.000000000;
759 | $dm++;
760 | }
761 | $tm += $dm;
762 | if($tm >= 60) {
763 | $tm = $tm - 60;
764 | $dh++;
765 | }
766 | $th += $dh;
767 | return sprintf("%02d:%02d:%02.9f",$th,$tm,$ts);
768 | }
769 |
770 | sub sys {
771 | my $self = shift;
772 | my @app = @_;
773 | my $buf;
774 | more();
775 | TRACE "sys > @app";
776 | run3(\@app, undef, sub {
777 | my $line = shift;
778 | TRACE "sys < $line";
779 | $buf .= $line;
780 | }, sub {
781 | my $line = shift;
782 | TRACE "sys !! $line";
783 | $buf .= $line;
784 | });
785 | less();
786 | return $buf;
787 | }
788 |
789 | sub more {
790 | Log::Log4perl::NDC->push(" ");
791 | }
792 |
793 | sub less {
794 | Log::Log4perl::NDC->pop();
795 | }
796 |
797 | }
798 |
--------------------------------------------------------------------------------
/dist/winport.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env perl
2 | # UnlinkMKV - Undo segment linking in MKV files
3 | # Garret Noling 2013-2015
4 | # Modifications by Vaughan Hilts for Win32 Compatability
5 |
6 | require 5.010;
7 | use strict;
8 | use XML::LibXML;
9 | use File::Glob qw/:globally :nocase/;
10 | use Math::BigFloat qw/:constant/;
11 | use Getopt::Long qw/:config passthrough/;
12 | use Log::Log4perl qw/:easy/;
13 | use File::Basename;
14 | use String::CRC32;
15 | use IPC::Run3;
16 | use Cwd qw/cwd realpath abs_path/;
17 | use File::Which; qw/which/;
18 | use File::Spec::Functions;
19 | use File::Path qw/mkpath/;
20 |
21 | my $loglevel = 'INFO';
22 | GetOptions (
23 | 'll|loglevel=s' => \$loglevel,
24 | );
25 | my $conf = qq(
26 | log4perl.logger = $loglevel, STDINF
27 | log4perl.appender.STDINF = Log::Log4perl::Appender::Screen
28 | log4perl.appender.STDINF.stderr = 0
29 | log4perl.appender.STDINF.layout = PatternLayout
30 | log4perl.appender.STDINF.layout.ConversionPattern = %x%m{chomp}%n
31 | );
32 | Log::Log4perl->init_once(\$conf);
33 | Log::Log4perl::NDC->push("");
34 | INFO "UnlinkMKV";
35 | UnlinkMKV::more();
36 |
37 | my $opt;
38 | my $basedir = canonpath(dirname(abs_path($0)));
39 | my $inifile = catfile($basedir, "unlinkmkv.ini");
40 | my $cwd = canonpath(cwd());
41 | $opt->{out_dir} = canonpath("$cwd/UMKV");
42 | $opt->{tmpdir} = canonpath("$cwd/UMKV/tmp");
43 | $opt->{ffmpeg} = "ffmpeg";
44 | $opt->{mkvext} = "mkvextract";
45 | $opt->{mkvinfo} = "mkvinfo";
46 | $opt->{mkvmerge} = "mkvmerge";
47 | $opt->{fixaudio} = 0;
48 | $opt->{fixvideo} = 0;
49 | $opt->{fixsubtitles} = 1;
50 | $opt->{ignoredefaultflag} = 0;
51 | $opt->{chapters} = 1;
52 | $opt->{playresx};
53 | $opt->{playresy};
54 |
55 | if(-f $inifile) {
56 | open my $F, $inifile or LOGDIE "failed to open unlinkmkv.ini: $!";
57 | while(my $line = <$F>) {
58 | chomp($line);
59 | $line =~ /^[\s\t]*([a-z0-9_]+)[\s\t]*=[\s\t]*["']?([^\s\t].*[^\s\t]?)["']?[\s\t]*$/;
60 | my $key = $1;
61 | my $val = $2;
62 | $val =~ s/\$basedir/$basedir/g;
63 | DEBUG "[ini] [$key] = [$val]";
64 | $opt->{$key} = $val;
65 | }
66 | close $F;
67 | }
68 |
69 | GetOptions ( \%$opt,
70 | 'tmpdir=s',
71 | 'fixaudio|fa!',
72 | 'fixvideo|fv!',
73 | 'fixsubtitles|fs!',
74 | 'outdir=s',
75 | 'playresx=i',
76 | 'playresy=i',
77 | 'ignoredefaultflag',
78 | 'chapters!',
79 | 'ffmpeg=s',
80 | 'mkvextract=s',
81 | 'mkvinfo=s',
82 | 'mkvmerge=s',
83 | );
84 |
85 | $opt->{out_dir} = canonpath($opt->{out_dir});
86 | $opt->{ffmpeg} = canonpath(abs_path(which $opt->{ffmpeg}));
87 | $opt->{mkvext} = canonpath(abs_path(which $opt->{mkvext}));
88 | $opt->{mkvinfo} = canonpath(abs_path(which $opt->{mkvinfo}));
89 | $opt->{mkvmerge} = canonpath(abs_path(which $opt->{mkvmerge}));
90 | $opt->{tmpdir} = canonpath($opt->{tmpdir});
91 |
92 | UnlinkMKV::more();
93 | INFO "Options";
94 | UnlinkMKV::more();
95 | foreach my $key (sort keys %$opt) {
96 | INFO "$key: $opt->{$key}";
97 | }
98 | UnlinkMKV::less();
99 | UnlinkMKV::less();
100 |
101 | my $UMKV = UnlinkMKV->new({
102 | ffmpeg => $opt->{ffmpeg},
103 | mkvext => $opt->{mkvext},
104 | mkvinfo => $opt->{mkvinfo},
105 | mkvmerge => $opt->{mkvmerge},
106 | tmp => $opt->{tmpdir},
107 | fixaudio => $opt->{fixaudio},
108 | fixvideo => $opt->{fixvideo},
109 | fixsubs => $opt->{fixsubtitles},
110 | outdir => $opt->{out_dir},
111 | playresx => $opt->{playresx},
112 | playresy => $opt->{playresy},
113 | ignoredefaultflag => $opt->{ignoredefaultflag},
114 | chapters => $opt->{chapters},
115 | });
116 |
117 | if(scalar(@ARGV) == 0) {
118 | push @ARGV, $cwd;
119 | }
120 |
121 | my @LIST;
122 | foreach my $item (@ARGV) {
123 | if(-d $item) {
124 | opendir my $D, $item;
125 | while (my $F = readdir($D)) {
126 | if (-f catfile($item, $F) && $F =~ /\.mkv$/i && !-f catfile($opt->{out_dir}, $F)) {
127 | push @LIST, canonpath(abs_path("$item/$F"));
128 | }
129 | }
130 | closedir $D;
131 | }
132 | elsif(-f $item) {
133 | push @LIST, canonpath(abs_path($item));
134 | }
135 | }
136 | do { $UMKV->process($_) } for @LIST;
137 | exit;
138 |
139 | package UnlinkMKV {
140 | use strict;
141 | use XML::LibXML;
142 | use File::Glob qw/:globally :nocase/;
143 | use Math::BigFloat qw/:constant/;
144 | use Getopt::Long qw/:config passthrough/;
145 | use Log::Log4perl qw/:easy/;
146 | use File::Basename;
147 | use String::CRC32;
148 | use IPC::Run3;
149 | use Cwd qw/cwd realpath abs_path/;
150 | use File::Path qw/make_path rmtree/;
151 | use File::Copy;
152 | use File::Spec::Functions;
153 |
154 | sub new {
155 | my $type = shift;
156 | my $opt = shift;
157 | my ($self) = {};
158 | bless($self, $type);
159 | $self->{opt} = $opt;
160 | $self->{xml} = XML::LibXML->new();
161 | $self->mktmp();
162 | return $self;
163 | }
164 |
165 | sub DESTROY {
166 | my $self = shift;
167 | if(-d $self->{tmp}) {
168 | chdir($self->{opt}->{outdir});
169 | rmtree($self->{tmp}, 0, 1);
170 | DEBUG "removed tmp $self->{tmp}";
171 | }
172 | DEBUG "exiting";
173 | }
174 |
175 | sub mktmp {
176 | my $self = shift;
177 | if(!defined $self->{opt}->{tmp}) {
178 | $self->{tmp} = canonpath(cwd()."/UnlinkMKV/tmp/$$");
179 | }
180 | else {
181 | $self->{tmp} = catfile($self->{opt}->{tmp}, $$);
182 | }
183 | if(-d $self->{tmp}) {
184 | rmtree($self->{tmp}, 0, 1);
185 | }
186 | DEBUG "removed tmp $self->{tmp}";
187 | $self->{attachdir} = catfile($self->{tmp}, 'attach');
188 | $self->{partsdir} = catfile($self->{tmp}, 'parts');
189 | $self->{encodesdir} = catfile($self->{tmp}, 'encodes');
190 | $self->{subtitlesdir} = catfile($self->{tmp}, 'subtitles');
191 | $self->{segmentsdir} = catfile($self->{tmp}, 'segments');
192 | make_path(
193 | $self->{tmp},
194 | $self->{attachdir},
195 | $self->{partsdir},
196 | $self->{encodesdir},
197 | $self->{subtitlesdir},
198 | $self->{segmentsdir},
199 | , { verbose => 0 });
200 | DEBUG "created tmp $self->{tmp}";
201 | }
202 |
203 | sub process {
204 | my $self = shift;
205 | my $item = shift;
206 | my $origpath = canonpath(dirname($item));
207 | chdir($origpath);
208 | INFO "processing $item";
209 | more();
210 | INFO "checking if file is segmented";
211 | if($self->is_linked($item)) {
212 | INFO "generating chapter file";
213 | more();
214 | my(@segments, @splits);
215 | my($parent, $dir, $suffix) = fileparse($item, qr/\.[mM][kK][vV]/);
216 | INFO "loading chapters";
217 | more();
218 | my $xml = $self->{xml}->load_xml(string => $self->sys($self->{opt}->{mkvext}, 'chapters', $item));
219 | open my $out_chapters_orig, '>', catfile($self->{tmp}, "$parent-chapters-original.xml");
220 | print {$out_chapters_orig} $xml->toString;
221 | close $out_chapters_orig;
222 | my $offs_time_end = '00:00:00.000000000';
223 | my $last_time_end = '00:00:00.000000000';
224 | my $offset = '00:00:00.000000000';
225 | my $chaptercount = 1;
226 | foreach my $edition ($xml->findnodes('//EditionFlagDefault[.=0]')) {
227 | if(!$self->{opt}->{ignoredefaultflag}) {
228 | $edition->parentNode->unbindNode;
229 | WARN "non-default chapter dropped";
230 | }
231 | else {
232 | INFO "non-default chapter kept on purpose";
233 | }
234 | }
235 | foreach my $chapter ($xml->findnodes('//ChapterAtom')) {
236 | my ($ChapterTimeStart) = $chapter->findnodes('./ChapterTimeStart/text()');
237 | my ($ChapterTimeEnd) = $chapter->findnodes('./ChapterTimeEnd/text()');
238 | my $ChapterEnabled = ($chapter->findvalue('ChapterFlagEnabled') =~ /^\d+$/) ? $chapter->findvalue('ChapterFlagEnabled') : 1;
239 | if($chapter->exists('ChapterSegmentUID') && $ChapterEnabled) {
240 | my ($SegmentUID, $SegmentELE, $SegmentUIDText);
241 | ($SegmentELE) = $chapter->findnodes('./ChapterSegmentUID');
242 | ($SegmentUID) = $chapter->findnodes('./ChapterSegmentUID/text()');
243 | $SegmentUIDText = $SegmentUID->textContent();
244 | if($SegmentELE->getAttribute('format') eq 'hex') {
245 | $SegmentUIDText =~ s/\n//g;
246 | $SegmentUIDText =~ s/\s//g;
247 | $SegmentUIDText =~ s/([a-zA-Z0-9]{2})/ 0x$1/g;
248 | $SegmentUIDText =~ s/^\s//;
249 | }
250 | elsif($SegmentELE->getAttribute('format') eq 'ascii') {
251 | $SegmentUIDText =~ s/(.)/sprintf("0x%x ",ord($1))/eg;
252 | $SegmentUIDText =~ s/\s$//;
253 | }
254 | push @segments, {
255 | start => $ChapterTimeStart->textContent(),
256 | stop => $ChapterTimeEnd->textContent(),
257 | id => $SegmentUIDText,
258 | split_start => $last_time_end
259 | };
260 | push @splits, $last_time_end unless $last_time_end eq '00:00:00.000000000';
261 | $offset = $self->add_duration_to_timecode($offset, $ChapterTimeEnd->textContent());
262 | if($offs_time_end eq '00:00:00.000000000' && $chaptercount > 1) {
263 | $ChapterTimeStart->setData($offset);
264 | $ChapterTimeEnd->setData($self->add_duration_to_timecode($offset, $ChapterTimeEnd->textContent()));
265 | }
266 | else {
267 | $ChapterTimeStart->setData($offs_time_end);
268 | $ChapterTimeEnd->setData($self->add_duration_to_timecode($offs_time_end, $ChapterTimeEnd->textContent()));
269 | }
270 | $offs_time_end = $ChapterTimeEnd->textContent();
271 | $chapter->removeChild($chapter->findnodes('./ChapterSegmentUID'));
272 | INFO "external";
273 | more();
274 | INFO "chstart " . $ChapterTimeStart->textContent();
275 | INFO "chend " . $ChapterTimeEnd->textContent();
276 | INFO "offset " . $offset;
277 | INFO "offte " . $offs_time_end;
278 | INFO "chen " . $ChapterEnabled;
279 | less();
280 | }
281 | else {
282 | eval { if(defined $ChapterTimeEnd->textContent() && defined $ChapterTimeStart->textContent()){}; 1 } or next;
283 | push @segments, {
284 | file => $self->setpart(basename($item), abs_path($item)),
285 | start => $ChapterTimeStart->textContent(),
286 | stop => $ChapterTimeEnd->textContent(),
287 | split_start => $ChapterTimeStart->textContent(),
288 | split_stop => $ChapterTimeEnd->textContent()
289 | };
290 | $last_time_end = $ChapterTimeEnd->textContent();
291 | $ChapterTimeStart->setData($self->add_duration_to_timecode($ChapterTimeStart->textContent(), $offset));
292 | $ChapterTimeEnd->setData($self->add_duration_to_timecode($ChapterTimeEnd->textContent(), $offset));
293 | $offs_time_end = $ChapterTimeEnd->textContent();
294 | INFO "internal";
295 | more();
296 | INFO "chstart " . $ChapterTimeStart->textContent();
297 | INFO "chend " . $ChapterTimeEnd->textContent();
298 | INFO "offset " . $offset;
299 | INFO "offte " . $offs_time_end;
300 | INFO "chen " . $ChapterEnabled;
301 | less();
302 | }
303 | $chaptercount++;
304 | }
305 | less();
306 |
307 | INFO "writing chapter temporary file";
308 | open my $out_chapters, '>', catfile($self->{tmp}, "$parent-chapters.xml");
309 | print {$out_chapters} $xml->toString;
310 | close $out_chapters;
311 |
312 | INFO "looking for segment parts";
313 | more();
314 | foreach my $mkv (<*.mkv>) {
315 | next if $mkv eq $item;
316 | $mkv = canonpath(abs_path(catfile($dir, $mkv)));
317 | my ($id, $dur) = $self->mkvinfo($mkv);
318 | for (@segments) {
319 | next unless defined $_->{id};
320 | if($_->{id} eq $id && basename($mkv) ne basename($item)) {
321 | $_->{file} = $self->setpart(basename($mkv), $mkv);
322 | INFO "found part $_->{file}";
323 | }
324 | }
325 | }
326 | less();
327 |
328 | INFO "checking that all required segments were found";
329 | my $okay_to_proceed = 1;
330 | for (@segments) {
331 | if(defined $_->{id} && !defined $_->{file}) {
332 | DEBUG "missing segment: $_->{id}";
333 | $okay_to_proceed = 0;
334 | }
335 | }
336 | if(!$okay_to_proceed) {
337 | WARN "missing segments!";
338 | return;
339 | }
340 |
341 | INFO "extracting attachments";
342 | more();
343 | foreach my $seg (@segments) {
344 | my $file = "$seg->{file}";
345 | my $in = 0;
346 | my ($N, $T, $D, $U);
347 | TRACE "FILE $file";
348 | for(split /\n/, $self->sys($self->{opt}->{mkvinfo}, $file)) {
349 | chomp;
350 | if ($_ =~ /^\| \+ Attached/) {
351 | $in = 1;
352 | }
353 | elsif($in && $_ =~ /File name: (.*)/) {
354 | $N = $1;
355 | }
356 | elsif($in && $_ =~ /Mime type: (.*)/) {
357 | $T = $1;
358 | }
359 | elsif($in && $_ =~ /File data, size: (.*)/) {
360 | $D = $1;
361 | }
362 | elsif($in && $_ =~ /File UID: (.*)/) {
363 | $U = $1;
364 | }
365 | if (defined $N && defined $T && defined $D && defined $U) {
366 | push @{$seg->{attachments}}, { name => $N, type => $T, data => $D, UID => $U };
367 | undef $N;
368 | undef $T;
369 | undef $D;
370 | undef $U;
371 | }
372 | }
373 | if (defined $seg->{attachments} && @{$seg->{attachments}} > 0) {
374 | my $dir = cwd();
375 | TRACE "chdir $self->{attachdir}";
376 | chdir($self->{attachdir});
377 | $self->sys($self->{opt}->{mkvext}, 'attachments', $file, (1..$#{$seg->{attachments}}+1));
378 | chdir($dir);
379 | }
380 | }
381 | less();
382 |
383 | my @atts;
384 | opendir my $D, $self->{attachdir} or LOGDIE "failed to open attachment directory: $!";
385 | while(my $item = readdir($D)) {
386 | my $F = catfile($self->{attachdir}, $item);
387 | if(-f $F) {
388 | push @atts, ('--attachment-mime-type', 'application/x-truetype-font', '--attach-file', $F);
389 | }
390 | }
391 | closedir $D;
392 |
393 | INFO "creating splits";
394 | more();
395 | if(scalar(@splits) > 0) {
396 | DEBUG "splitting file: $item";
397 | $self->sys($self->{opt}->{mkvmerge}, '--no-chapters', '-o', catfile($self->{partsdir}, "split-%03d.mkv"), $item, '--split', 'timecodes:' . join(',',@splits));
398 | }
399 | less();
400 |
401 | INFO "setting parts";
402 | more();
403 | my (@parts, $LAST);
404 | my $count = 1;
405 | foreach my $segment (@segments) {
406 | if(defined $segment->{id} && $segment->{start} =~ /^00:00:00\./ || ($LAST ne $segment->{file} && scalar(@splits) == 0)) {
407 | DEBUG "part $segment->{file}";
408 | push @parts, $segment->{file};
409 | }
410 | elsif($LAST ne $segment->{file}) {
411 | my $f = catfile($self->{partsdir}, sprintf("split-%03d.mkv",$count));
412 | DEBUG "part $f";
413 | push @parts, $f;
414 | $count++;
415 | }
416 | $LAST = $segment->{file};
417 | }
418 | less();
419 |
420 | my $subs;
421 | if($self->{opt}->{fixsubs}) {
422 | INFO "extracting subs";
423 | more();
424 | foreach my $part (@parts) {
425 | DEBUG "$part";
426 | my $in = 0;
427 | my $sub = 0;
428 | my ($N, $T, $D, $U);
429 | for (split /\n/, $self->sys($self->{opt}->{mkvinfo}, $part)) {
430 | chomp;
431 | if ($_ =~ /^\| \+ A track/) {
432 | $in = 1;
433 | undef $N;
434 | undef $T;
435 | undef $D;
436 | undef $U;
437 | $sub = 0;
438 | }
439 | elsif($in && $_ =~ /Track type: subtitles/) {
440 | $sub = 1;
441 | }
442 | elsif($in && $_ =~ /Track number: .*: (\d)\)$/) {
443 | $T = $1;
444 | }
445 | if (defined $in && $sub && $T) {
446 | my $sf = catfile($self->{subtitlesdir}, basename($part)."-$T.ass");
447 | $self->sys($self->{opt}->{mkvext}, 'tracks', $part, "$T:$sf");
448 | push @{$subs->{$part}}, $sf;
449 | undef $T;
450 | $in = 0;
451 | $sub = 0;
452 | }
453 | }
454 | }
455 | less();
456 |
457 | INFO "making substyles unique";
458 | more();
459 | my $styles;
460 | foreach my $f (keys %$subs) {
461 | push @$styles, @{$self->uniquify_substyles($subs->{$f})};
462 | }
463 | less();
464 |
465 | INFO "mashing unique substyles to all parts";
466 | more();
467 | foreach my $f (keys %$subs) {
468 | $self->mush_substyles($subs->{$f}, $styles);
469 | }
470 | less();
471 |
472 | INFO "remuxing subtitles";
473 | more();
474 | foreach my $f (keys %$subs) {
475 | DEBUG $f;
476 | my @stracks;
477 | foreach my $T (@{$subs->{$f}}) {
478 | push @stracks, $T;
479 | }
480 | $self->sys($self->{opt}->{mkvmerge}, '-o', "$f-fixsubs.mkv", '--no-chapters', '--no-subs', $f, @stracks, @atts);
481 | $self->replace($f, "$f-fixsubs.mkv");
482 | }
483 | less();
484 | }
485 |
486 | if($self->{opt}->{fixvideo} || $self->{opt}->{fixaudio}) {
487 | INFO "encoding parts";
488 | more();
489 | foreach my $part (@parts) {
490 | my @vopt = qw/-vcodec copy/;
491 | my @aopt = qw/-map 0 -acodec copy/;
492 | WARN $part;
493 | if ($self->{opt}->{fixvideo}) {
494 | my $br = $self->bitrate($part);
495 | @vopt = undef;
496 | @vopt = ('-c:v', 'libx264', '-b:v', $br.'k', '-minrate', $br.'k', '-maxrate', ($br*2).'k', '-bufsize', '1835k');
497 | }
498 | if ($self->{opt}->{fixaudio}) {
499 | @aopt = undef;
500 | @aopt = qw/-map 0 -acodec ac3 -ab 320k/;
501 | }
502 | $self->sys($self->{opt}->{ffmpeg}, '-i', $part, @vopt, @aopt, "$part-fixed.mkv");
503 | $self->replace($part, "$part-fixed.mkv");
504 | }
505 | less();
506 | }
507 |
508 | INFO "building file";
509 | more();
510 | my @PRTS;
511 | foreach my $part (@parts) {
512 | push @PRTS, $part;
513 | push @PRTS, '+';
514 | }
515 | pop @PRTS;
516 | if($self->{opt}->{chapters}) {
517 | $self->sys($self->{opt}->{mkvmerge}, '--no-chapters', '-M', '--chapters', catfile($self->{tmp}, "$parent-chapters.xml"), '-o', catfile($self->{encodesdir}, basename($item)), @PRTS);
518 | }
519 | else {
520 | $self->sys($self->{opt}->{mkvmerge}, '--no-chapters', '-M', '--chapters', catfile($self->{tmp}, "$parent-chapters.xml"), '-o', catfile($self->{encodesdir}, basename($item)), @PRTS);
521 | }
522 | less();
523 |
524 | INFO "fixing subs, again... (maybe an mkvmerge issue?)";
525 | more();
526 | if($self->{opt}->{fixsubs}) {
527 | my @FS;
528 | my $in = 0;
529 | my $sub = 0;
530 | my ($N, $T, $D, $U);
531 | for (split /\n/, $self->sys($self->{opt}->{mkvinfo}, catfile($self->{encodesdir}, basename($item)))) {
532 | chomp;
533 | if ($_ =~ /^\| \+ A track/) {
534 | $in = 1;
535 | undef $N;
536 | undef $T;
537 | undef $D;
538 | undef $U;
539 | $sub = 0;
540 | }
541 | elsif($in && $_ =~ /Track type: subtitles/) {
542 | $sub = 1;
543 | }
544 | elsif($in && $_ =~ /Track number: .*: (\d)\)$/) {
545 | $T = $1;
546 | }
547 | if (defined $in && $sub && $T) {
548 | $self->sys($self->{opt}->{mkvext}, 'tracks', catfile($self->{encodesdir}, basename($item)), "$T:".catfile($self->{encodesdir}, "$T.ass"));
549 | push @FS, catfile($self->{encodesdir}, "$T.ass");
550 | undef $T;
551 | $in = 0;
552 | $sub = 0;
553 | }
554 | }
555 | $self->sys($self->{opt}->{mkvmerge}, '-o', catfile($self->{encodesdir}, "fixed." . basename($item)), '-S', catfile($self->{encodesdir}, basename($item)), @FS);
556 | $self->replace(catfile($self->{encodesdir}, basename($item)), catfile($self->{encodesdir}, "fixed." . basename($item)));
557 | }
558 | less();
559 |
560 | INFO "moving built file to final destination";
561 | more();
562 | make_path($self->{opt}->{outdir}, { verbose => 0 });
563 | move(catfile($self->{encodesdir}, basename($item)), $self->{opt}->{outdir});
564 | less();
565 | }
566 | $self->mktmp();
567 | less();
568 | }
569 |
570 | sub bitrate {
571 | my $self = shift;
572 | my $file = shift;
573 | my $size = int(((-s $file)/1024+.5)*1.1);
574 | my $br = 2000;
575 | foreach my $line (split /\n/, $self->sys($self->{opt}->{ffmpeg}, '-i', $file)) {
576 | if($line =~ /duration: (\d+):(\d+):(\d+\.\d+),/i) {
577 | my $duration = ($1*3600)+($2*60)+int($3+.5);
578 | $br = int(($size / $duration)+.5);
579 | DEBUG "duration [$1:$2:$3] = $duration seconds, fsize $size = ${br}k bitrate";
580 | }
581 | if($line =~ /duration.*bitrate: (\d+) k/i) {
582 | $br = $1;
583 | DEBUG "original bitrate $br";
584 | }
585 | }
586 | return $br;
587 | }
588 |
589 | sub replace {
590 | my $self = shift;
591 | my $dest = shift;
592 | my $source = shift;
593 | unlink($dest);
594 | move($source, $dest);
595 | }
596 |
597 | sub is_linked {
598 | my $self = shift;
599 | my $item = shift;
600 | my $linked = 0;
601 | more();
602 | foreach my $line (split /\n/, $self->sys($self->{opt}->{mkvext}, 'chapters', $item)) {
603 | if($line =~ /{partsdir}, $link);
622 | DEBUG "copying part $file to $part";
623 | copy($file, $part);
624 | return $part;
625 | }
626 |
627 | sub uniquify_substyles {
628 | my $self = shift;
629 | my $S = shift;
630 | my @styles;
631 | foreach my $T (@$S) {
632 | DEBUG $T;
633 | my $uniq = crc32($T);
634 | open my $O, '>', "$T.new";
635 | open my $F, '<', $T;
636 | my $in = 0;
637 | my $di = 0;
638 | my $key;
639 | while(my $line = <$F>) {
640 | if ($line =~ /^\[/ && $line =~ /^\[V4\+ Styles/) {
641 | $in = 1;
642 | }
643 | elsif(($in||$di) && !defined $key && $line =~ /^Format:/i) {
644 | my $test = "$line";
645 | $test =~ s/ //g;
646 | $test =~ s/^format://i;
647 | my(@parts) = split /,/, $test;
648 | my $c = 0;
649 | foreach my $part (@parts) {
650 | if ($in && $part =~ /^name$/i) {
651 | $key = $c;
652 | }
653 | elsif($di && $part =~ /^style$/i) {
654 | $key = $c;
655 | }
656 | $c++;
657 | }
658 | }
659 | elsif($in && defined $key && $line =~ /^style:/i) {
660 | $line =~ s/^style:\s+?//i;
661 | my(@parts) = split /,/, $line;
662 | $parts[$key] = "$parts[$key] u$uniq";
663 | $line = "Style: " . join(',', @parts);
664 | push @styles, $line;
665 | DEBUG $line;
666 | }
667 | elsif($line =~ /^\[Events/i) {
668 | $in = 0;
669 | $di = 1;
670 | $key = undef;
671 | }
672 | elsif($di && defined $key && $line =~ /^dialogue:/i) {
673 | $line =~ s/^dialogue: //i;
674 | my(@parts) = split /,/, $line;
675 | $parts[$key] = "$parts[$key] u$uniq";
676 | $line = "Dialogue: " . join(',', @parts);
677 | }
678 | print $O $line;
679 | }
680 | close $F;
681 | close $O;
682 | unlink $T;
683 | move("$T.new", $T);
684 | }
685 | return \@styles;
686 | }
687 |
688 | sub mush_substyles {
689 | my $self = shift;
690 | my $S = shift;
691 | my $styles = shift;
692 | foreach my $T (@$S) {
693 | open my $F, '<', $T;
694 | my @lines = <$F>;
695 | close $F;
696 | open my $F, '>', $T;
697 | my $in = 0;
698 | foreach my $line (@lines) {
699 | if ($line =~ /^\[/ && $line =~ /^\[V4\+ Styles/) {
700 | $in = 1;
701 | print $F $line;
702 | }
703 | elsif($in && $line =~ /^format:/i) {
704 | print $F $line;
705 | do {
706 | print $F $_;
707 | } for @$styles;
708 | }
709 | elsif($in && $line =~ /^style:/i) {
710 | #do nothing
711 | }
712 | elsif($in && $line =~ /^\[/) {
713 | $in = 0;
714 | print $F $line;
715 | }
716 | elsif(defined $self->{opt}->{playresx} && $line =~ /^PlayResX:/) {
717 | print $F "PlayResX: $self->{opt}->{playresx}\n";
718 | }
719 | elsif(defined $self->{opt}->{playresy} && $line =~ /^PlayResY:/) {
720 | print $F "PlayResY: $self->{opt}->{playresy}\n";
721 | }
722 | else {
723 | print $F $line;
724 | }
725 | }
726 | close $F;
727 | }
728 | }
729 |
730 | sub mkvinfo {
731 | my $self = shift;
732 | my $file = shift;
733 | my $id = '';
734 | my $dur = '';
735 | for(split /\n/, $self->sys($self->{opt}->{mkvinfo}, $file)) {
736 | chomp;
737 | if ($_ =~ /Segment[ \-]UID:/) {
738 | $_ =~ /Segment[ \-]UID:([a-zA-Z0-9\s]+)$/;
739 | $id = $1;
740 | $id =~ s/^\s+//g;
741 | $id =~ s/\s+$//g;
742 | }
743 | elsif($_ =~ /^\| \+ Duration:/) {
744 | $_ =~ /^\| \+ Duration:.*\((.*)\)/;
745 | $dur = $1;
746 | }
747 | }
748 | return ($id, $dur);
749 | }
750 |
751 | sub add_duration_to_timecode {
752 | my $self = shift;
753 | my $time = shift;
754 | my $dur = shift;
755 | my ($th, $tm, $ts) = split /:/, $time;
756 | my ($dh, $dm, $ds) = split /:/, $dur;
757 | $ts += ($ds + 0.000000001);
758 | if($ts >= 60.000000000) {
759 | $ts = $ts - 60.000000000;
760 | $dm++;
761 | }
762 | $tm += $dm;
763 | if($tm >= 60) {
764 | $tm = $tm - 60;
765 | $dh++;
766 | }
767 | $th += $dh;
768 | return sprintf("%02d:%02d:%02.9f",$th,$tm,$ts);
769 | }
770 |
771 | sub sys {
772 | my $self = shift;
773 | my @app = @_;
774 | my $buf;
775 | more();
776 | TRACE "sys > @app";
777 | run3(\@app, undef, sub {
778 | my $line = shift;
779 | TRACE "sys < $line";
780 | $buf .= $line;
781 | }, sub {
782 | my $line = shift;
783 | TRACE "sys !! $line";
784 | $buf .= $line;
785 | });
786 | less();
787 | return $buf;
788 | }
789 |
790 | sub more {
791 | Log::Log4perl::NDC->push(" ");
792 | }
793 |
794 | sub less {
795 | Log::Log4perl::NDC->pop();
796 | }
797 |
798 | }
--------------------------------------------------------------------------------