Initial commit: Futonizer MKV track stripper
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
namespace Futonizer.Models;
|
||||
|
||||
public class AppSettings
|
||||
{
|
||||
public string MkvMergePath { get; set; } = string.Empty;
|
||||
public string OutputFolder { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Futonizer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// One record of a previously-run strip (or copy) job, persisted so it can
|
||||
/// be reviewed later from Settings.
|
||||
/// </summary>
|
||||
public class HistoryEntry
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool Failed => !Success;
|
||||
|
||||
[JsonIgnore]
|
||||
public string TimestampDisplay => Timestamp.ToString("yyyy-MM-dd HH:mm");
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Futonizer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one currently-running strip job for display in the "active jobs" list,
|
||||
/// since multiple files can be stripped concurrently.
|
||||
/// </summary>
|
||||
public class JobProgressItem : INotifyPropertyChanged
|
||||
{
|
||||
private int _percent;
|
||||
private double _megabytesPerSecond;
|
||||
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
public int Percent
|
||||
{
|
||||
get => _percent;
|
||||
set
|
||||
{
|
||||
_percent = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(Display));
|
||||
}
|
||||
}
|
||||
|
||||
public double MegabytesPerSecond
|
||||
{
|
||||
get => _megabytesPerSecond;
|
||||
set
|
||||
{
|
||||
_megabytesPerSecond = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(Display));
|
||||
}
|
||||
}
|
||||
|
||||
public string Display => $"{FileName} - {Percent}% - {MegabytesPerSecond:F1} MB/s";
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Futonizer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Root object returned by `mkvmerge -J <file>`.
|
||||
/// Only the fields we actually need are mapped.
|
||||
/// </summary>
|
||||
public class MkvIdentifyResult
|
||||
{
|
||||
[JsonPropertyName("file_name")]
|
||||
public string? FileName { get; set; }
|
||||
|
||||
[JsonPropertyName("tracks")]
|
||||
public List<MkvTrack> Tracks { get; set; } = new();
|
||||
}
|
||||
|
||||
public class MkvTrack
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("codec")]
|
||||
public string Codec { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("properties")]
|
||||
public MkvTrackProperties? Properties { get; set; }
|
||||
}
|
||||
|
||||
public class MkvTrackProperties
|
||||
{
|
||||
[JsonPropertyName("language")]
|
||||
public string? Language { get; set; }
|
||||
|
||||
[JsonPropertyName("language_ietf")]
|
||||
public string? LanguageIetf { get; set; }
|
||||
|
||||
[JsonPropertyName("track_name")]
|
||||
public string? TrackName { get; set; }
|
||||
|
||||
[JsonPropertyName("default_track")]
|
||||
public bool DefaultTrack { get; set; }
|
||||
|
||||
[JsonPropertyName("forced_track")]
|
||||
public bool ForcedTrack { get; set; }
|
||||
|
||||
[JsonPropertyName("audio_channels")]
|
||||
public int? AudioChannels { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Futonizer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The four states shown as an icon in the file queue list.
|
||||
/// </summary>
|
||||
public enum FileLoadState
|
||||
{
|
||||
/// <summary>Currently being scanned with mkvmerge -J.</summary>
|
||||
Loading,
|
||||
/// <summary>Scan finished (or failed) but track selection isn't evaluated as correct/incorrect yet.</summary>
|
||||
Loaded,
|
||||
/// <summary>Exactly one audio track and one subtitle track are selected to be kept.</summary>
|
||||
Correct,
|
||||
/// <summary>Anything other than exactly one audio + one subtitle selected.</summary>
|
||||
Incorrect,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one MKV file in the processing queue.
|
||||
/// </summary>
|
||||
public class QueuedFileItem : INotifyPropertyChanged
|
||||
{
|
||||
private string _status = string.Empty;
|
||||
private bool _isScanning;
|
||||
private bool _hasError;
|
||||
|
||||
public string FilePath { get; }
|
||||
public string FileName => Path.GetFileName(FilePath);
|
||||
public ObservableCollection<TrackItem> Tracks { get; } = new();
|
||||
|
||||
public string Status
|
||||
{
|
||||
get => _status;
|
||||
set { _status = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public bool IsScanning
|
||||
{
|
||||
get => _isScanning;
|
||||
set { _isScanning = value; OnPropertyChanged(); NotifyLoadState(); }
|
||||
}
|
||||
|
||||
public bool HasError
|
||||
{
|
||||
get => _hasError;
|
||||
set { _hasError = value; OnPropertyChanged(); NotifyLoadState(); }
|
||||
}
|
||||
|
||||
public FileLoadState LoadState
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isScanning) return FileLoadState.Loading;
|
||||
if (_hasError || Tracks.Count == 0) return FileLoadState.Loaded;
|
||||
|
||||
int audioSelected = Tracks.Count(t => t.Type == "audio" && t.Copy);
|
||||
int subtitleSelected = Tracks.Count(t => t.Type == "subtitles" && t.Copy);
|
||||
|
||||
return audioSelected == 1 && subtitleSelected == 1
|
||||
? FileLoadState.Correct
|
||||
: FileLoadState.Incorrect;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLoading => LoadState == FileLoadState.Loading;
|
||||
public bool IsLoadedNeutral => LoadState == FileLoadState.Loaded;
|
||||
public bool IsCorrect => LoadState == FileLoadState.Correct;
|
||||
public bool IsIncorrect => LoadState == FileLoadState.Incorrect;
|
||||
|
||||
public QueuedFileItem(string filePath)
|
||||
{
|
||||
FilePath = filePath;
|
||||
Tracks.CollectionChanged += OnTracksChanged;
|
||||
}
|
||||
|
||||
private void OnTracksChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.NewItems != null)
|
||||
{
|
||||
foreach (TrackItem t in e.NewItems)
|
||||
t.PropertyChanged += TrackPropertyChanged;
|
||||
}
|
||||
|
||||
if (e.OldItems != null)
|
||||
{
|
||||
foreach (TrackItem t in e.OldItems)
|
||||
t.PropertyChanged -= TrackPropertyChanged;
|
||||
}
|
||||
|
||||
NotifyLoadState();
|
||||
}
|
||||
|
||||
private void TrackPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(TrackItem.Copy))
|
||||
NotifyLoadState();
|
||||
}
|
||||
|
||||
private void NotifyLoadState()
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LoadState)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLoading)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLoadedNeutral)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsCorrect)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsIncorrect)));
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Futonizer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one track of a queued MKV file, as shown in the track table.
|
||||
/// The <see cref="Copy"/> property is user-editable and drives which tracks
|
||||
/// are passed to mkvmerge when stripping.
|
||||
/// </summary>
|
||||
public class TrackItem : INotifyPropertyChanged
|
||||
{
|
||||
private bool _copy;
|
||||
|
||||
public int Id { get; init; }
|
||||
public string Type { get; init; } = string.Empty;
|
||||
public string Codec { get; init; } = string.Empty;
|
||||
public string Language { get; init; } = string.Empty;
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public bool ForcedDisplay { get; init; }
|
||||
|
||||
private bool _defaultTrack;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this track should be flagged as the default track of its type
|
||||
/// when stripped. For subtitles this is kept in sync with <see cref="Copy"/>
|
||||
/// (selecting a subtitle also makes it the default one).
|
||||
/// </summary>
|
||||
public bool DefaultTrack
|
||||
{
|
||||
get => _defaultTrack;
|
||||
set
|
||||
{
|
||||
if (_defaultTrack == value) return;
|
||||
_defaultTrack = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Text shown in the track table's Name column. Subtitle tracks are shown
|
||||
/// as "Codec/Name" so tracks that share a display name but use a different
|
||||
/// codec are easy to tell apart.
|
||||
/// </summary>
|
||||
public string DisplayName => Type == "subtitles" ? $"{Codec}/{Name}" : Name;
|
||||
|
||||
public bool Copy
|
||||
{
|
||||
get => _copy;
|
||||
set
|
||||
{
|
||||
if (_copy == value) return;
|
||||
_copy = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Key used to match subtitle tracks with the same identity across different
|
||||
/// queued files when propagating a subtitle selection. Subtitles are matched
|
||||
/// on "Codec/Name" so tracks with the same name but a different codec are
|
||||
/// treated as distinct.
|
||||
/// </summary>
|
||||
public string PropagationKey => DisplayName;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
Reference in New Issue
Block a user