├── 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 | } --------------------------------------------------------------------------------