diff --git a/Base.lproj/QueueOptions.xib b/Base.lproj/QueueOptions.xib
index 53878e7..30e19a0 100644
--- a/Base.lproj/QueueOptions.xib
+++ b/Base.lproj/QueueOptions.xib
@@ -16,7 +16,7 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -772,7 +893,22 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -799,7 +935,7 @@
-
+
diff --git a/Classes/DocumentWindowController.swift b/Classes/DocumentWindowController.swift
index bcf3934..3579e83 100644
--- a/Classes/DocumentWindowController.swift
+++ b/Classes/DocumentWindowController.swift
@@ -308,17 +308,7 @@ final class DocumentWindowController: NSWindowController, TracksViewControllerDe
}
@IBAction func addChaptersEvery(_ sender: NSMenuItem) {
- let track: MP42ChapterTrack = mp4.chapters ?? { let track = MP42ChapterTrack(); self.mp4.addTrack(track); return track }()
- let minutes = sender.tag * 60 * 1000
-
- if minutes > 0 {
- for (index, timestamp) in stride(from: 0, to: mp4.duration, by: minutes).enumerated() {
- track.addChapter("Chapter \(index + 1)", timestamp: UInt64(timestamp))
- }
- }
- else {
- track.addChapter("Chapter 1", timestamp: 0)
- }
+ mp4.addChapters(everyMinutes: sender.tag)
doc.updateChangeCount(.changeDone)
tracksViewController.reloadData()
diff --git a/Classes/MP42FileAdditions.swift b/Classes/MP42FileAdditions.swift
index 1e25644..96c0ff1 100644
--- a/Classes/MP42FileAdditions.swift
+++ b/Classes/MP42FileAdditions.swift
@@ -18,6 +18,39 @@ private extension String {
extension MP42File {
+ /// Policy for inserting chapter markers when a chapter track already exists.
+ enum ChaptersInsertPolicy: Int {
+ /// Clear existing markers, then insert interval chapters.
+ case replace = 0
+ /// Keep existing markers and append interval chapters.
+ case merge = 1
+ }
+
+ /// Adds chapter markers at a fixed interval, matching the document window "Insert a chapter every…" action.
+ /// - Parameter minutes: Interval in minutes. Values `<= 0` insert a single chapter at the beginning.
+ /// - Parameter policy: Whether to replace or merge with existing chapter markers. Defaults to `.merge`.
+ func addChapters(everyMinutes minutes: Int, policy: ChaptersInsertPolicy = .merge) {
+ let track: MP42ChapterTrack = chapters ?? {
+ let track = MP42ChapterTrack()
+ addTrack(track)
+ return track
+ }()
+
+ if policy == .replace, track.chapterCount() > 0 {
+ track.removeChapters(at: IndexSet(integersIn: 0.. 0 {
+ for (index, timestamp) in stride(from: 0, to: duration, by: interval).enumerated() {
+ track.addChapter("Chapter \(index + 1)", timestamp: UInt64(timestamp))
+ }
+ } else {
+ track.addChapter("Chapter 1", timestamp: 0)
+ }
+ }
+
enum TrackHDType : Int {
case hd720p = 1
case hd1080p = 2
diff --git a/Classes/QueueAction.swift b/Classes/QueueAction.swift
index 72783a0..9cafcaf 100644
--- a/Classes/QueueAction.swift
+++ b/Classes/QueueAction.swift
@@ -472,8 +472,8 @@ class QueuePrettifyAudioTrackNameAction : NSObject, QueueActionProtocol {
class QueueRenameChaptersAction : NSObject, QueueActionProtocol {
var type: QueueActionType { return .pre }
- var localizedDescription: String { return NSLocalizedString("Prettifying audio track names", comment: "Action localized description") }
- override var description: String { return NSLocalizedString("Prettify audio track names", comment: "Action description") }
+ var localizedDescription: String { return NSLocalizedString("Renaming chapters titles", comment: "Action localized description") }
+ override var description: String { return NSLocalizedString("Rename chapters titles", comment: "Action description") }
override init() {}
@@ -496,6 +496,45 @@ class QueueRenameChaptersAction : NSObject, QueueActionProtocol {
}
+/// An action that inserts chapter markers at a fixed interval.
+class QueueAddChaptersAction : NSObject, QueueActionProtocol {
+
+ var type: QueueActionType { return .pre }
+ var localizedDescription: String { return NSLocalizedString("Adding chapters", comment: "Action localized description") }
+ override var description: String { return NSLocalizedString("Add chapters", comment: "Action description") }
+
+ let minutes: Int
+ let policy: MP42File.ChaptersInsertPolicy
+
+ init(minutes: Int, policy: MP42File.ChaptersInsertPolicy = .replace) {
+ self.minutes = minutes
+ self.policy = policy
+ }
+
+ func runAction(_ item: QueueItem) -> Bool {
+ guard let mp4 = item.mp4File else { return false }
+ mp4.addChapters(everyMinutes: minutes, policy: policy)
+ return true
+ }
+
+ func encode(with aCoder: NSCoder) {
+ aCoder.encode(Int32(minutes), forKey: "SBQueueAddChaptersActionMinutes")
+ aCoder.encode(Int32(policy.rawValue), forKey: "SBQueueAddChaptersActionPolicy")
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ self.minutes = Int(aDecoder.decodeInt32(forKey: "SBQueueAddChaptersActionMinutes"))
+ if aDecoder.containsValue(forKey: "SBQueueAddChaptersActionPolicy") {
+ self.policy = MP42File.ChaptersInsertPolicy(rawValue: Int(aDecoder.decodeInt32(forKey: "SBQueueAddChaptersActionPolicy"))) ?? .replace
+ } else {
+ self.policy = .replace
+ }
+ }
+
+ static var supportsSecureCoding: Bool { return true }
+
+}
+
enum QueueColorSpaceActionTag: UInt16 {
case None = 1
case Rec601PAL
diff --git a/Classes/QueueController.swift b/Classes/QueueController.swift
index a7fe81f..ec3a098 100644
--- a/Classes/QueueController.swift
+++ b/Classes/QueueController.swift
@@ -284,9 +284,16 @@ final class QueueController : NSWindowController, NSWindowDelegate, NSPopoverDel
if prefs.prettifyAudioTrackName {
item.addAction(QueuePrettifyAudioTrackNameAction())
}
+ // Create chapters before rename/preview so markers exist when previews are generated.
+ if prefs.addChapters {
+ let policy = MP42File.ChaptersInsertPolicy(rawValue: prefs.addChaptersPolicy) ?? .replace
+ item.addAction(QueueAddChaptersAction(minutes: prefs.addChaptersInterval, policy: policy))
+ }
if prefs.renameChapters {
item.addAction(QueueRenameChaptersAction())
}
+ // Previews when explicitly requested, or whenever the queue is creating chapters.
+ item.setChaptersPreviewGeneration(prefs.chaptersPreview || prefs.addChapters)
if prefs.fixTrackLanguage {
item.addAction(QueueSetLanguageAction(language: prefs.fixTrackLanguageValue))
}
@@ -563,6 +570,7 @@ final class QueueController : NSWindowController, NSWindowDelegate, NSPopoverDel
else { return }
if popover == closedPopover {
+ prefs.saveUserDefaults()
popover = nil
}
if itemPopover == closedPopover {
@@ -571,6 +579,7 @@ final class QueueController : NSWindowController, NSWindowDelegate, NSPopoverDel
}
func windowWillClose(_ notification: Notification) {
+ prefs.saveUserDefaults()
windowController = nil
}
diff --git a/Classes/QueueItem.swift b/Classes/QueueItem.swift
index 3effaf1..ae4decd 100644
--- a/Classes/QueueItem.swift
+++ b/Classes/QueueItem.swift
@@ -77,11 +77,6 @@ import MP42Foundation
attributes[MP4264BitData] = true
}
- if Prefs.chaptersPreviewTrack {
- attributes[MP42GenerateChaptersPreviewTrack] = true
- attributes[MP42ChaptersPreviewPosition] = Prefs.chaptersPreviewPosition
- }
-
if Prefs.forceHvc1 {
attributes[MP42ForceHvc1] = true
}
@@ -138,6 +133,31 @@ import MP42Foundation
didChangeValue(for: \.actions)
}
+ /// Enables or disables chapter preview image generation using the General preference for position.
+ func setChaptersPreviewGeneration(_ enabled: Bool) {
+ queue.sync {
+ if enabled {
+ attributes[MP42GenerateChaptersPreviewTrack] = true
+ attributes[MP42ChaptersPreviewPosition] = Prefs.chaptersPreviewPosition
+ } else {
+ attributes.removeValue(forKey: MP42GenerateChaptersPreviewTrack)
+ attributes.removeValue(forKey: MP42ChaptersPreviewPosition)
+ }
+ }
+ }
+
+ private func updateChaptersPreviewGeneration(for mp4: MP42File) {
+ let hasChapters = (mp4.chapters?.chapterCount() ?? 0) > 0
+ let previewRequested = queue.sync { attributes[MP42GenerateChaptersPreviewTrack] != nil }
+ let createdChapters = actions.contains { $0 is QueueAddChaptersAction }
+
+ if hasChapters && (previewRequested || createdChapters) {
+ setChaptersPreviewGeneration(true)
+ } else {
+ setChaptersPreviewGeneration(false)
+ }
+ }
+
// MARK: Item processing
enum ProcessError: Error {
@@ -305,6 +325,11 @@ import MP42Foundation
guard let mp4 = mp4File else { return }
+ // Chapter previews run at the end of write/update. Create-chapters is a pre-action,
+ // so after prepare() we only keep preview generation when markers actually exist.
+ // Missing chapters must not fail the queue item.
+ updateChaptersPreviewGeneration(for: mp4)
+
#if SB_SANDBOX
let mp4Token = MP42SecurityAccessToken(object: mp4)
#endif
@@ -457,10 +482,11 @@ import MP42Foundation
QueueMetadataAction.classForCoder(), QueueSubtitlesAction.classForCoder(),
QueueSetLanguageAction.classForCoder(), QueueFixFallbacksAction.classForCoder(),
QueueClearTrackNameAction.classForCoder(), QueuePrettifyAudioTrackNameAction.classForCoder(),
- QueueRenameChaptersAction.classForCoder(),
+ QueueRenameChaptersAction.classForCoder(), QueueAddChaptersAction.classForCoder(),
QueueOrganizeGroupsAction.classForCoder(), QueueColorSpaceAction.classForCoder(),
QueueSetOutputFilenameAction.classForCoder(), QueueClearExistingMetadataAction.classForCoder(),
- QueueOptimizeAction.classForCoder(), QueueSendToiTunesAction.classForCoder()], forKey: "SBQueueItemActions") as! [QueueActionProtocol]
+ QueueOptimizeAction.classForCoder(), QueueSendToiTunesAction.classForCoder(),
+ QueueChangeAudioLanguageAction.classForCoder(), QueueChangeSubtitleLanguageAction.classForCoder()], forKey: "SBQueueItemActions") as! [QueueActionProtocol]
}
}
diff --git a/Classes/QueuePreferences.swift b/Classes/QueuePreferences.swift
index 544e48c..6636601 100644
--- a/Classes/QueuePreferences.swift
+++ b/Classes/QueuePreferences.swift
@@ -15,6 +15,10 @@ final class QueuePreferences: NSObject {
static private let SBQueueClearTrackName: String = "SBQueueClearTrackName"
static private let SBQueuePrettifyAudioTrackName: String = "SBQueuePrettifyAudioTrackName"
static private let SBQueueRenameChapters: String = "SBQueueRenameChapters"
+ static private let SBQueueAddChapters: String = "SBQueueAddChapters"
+ static private let SBQueueAddChaptersInterval: String = "SBQueueAddChaptersInterval"
+ static private let SBQueueAddChaptersPolicy: String = "SBQueueAddChaptersPolicy"
+ static private let SBQueueChaptersPreview: String = "SBQueueChaptersPreview"
static private let SBQueueMetadata: String = "SBQueueMetadata"
static private let SBQueueSubtitles: String = "SBQueueSubtitles"
static private let SBQueueSet: String = "SBQueueSet"
@@ -62,6 +66,11 @@ final class QueuePreferences: NSObject {
@objc dynamic var clearTrackName: Bool
@objc dynamic var prettifyAudioTrackName: Bool
@objc dynamic var renameChapters: Bool
+ @objc dynamic var addChapters: Bool
+ @objc dynamic var addChaptersInterval: Int
+ /// `MP42File.ChaptersInsertPolicy` raw value: 0 = replace, 1 = merge.
+ @objc dynamic var addChaptersPolicy: Int
+ @objc dynamic var chaptersPreview: Bool
@objc dynamic var subtitles: Bool
@objc dynamic var metadataSet: MetadataPreset?
@@ -106,6 +115,10 @@ final class QueuePreferences: NSObject {
self.clearTrackName = ud.bool(forKey: QueuePreferences.SBQueueClearTrackName)
self.prettifyAudioTrackName = ud.bool(forKey: QueuePreferences.SBQueuePrettifyAudioTrackName)
self.renameChapters = ud.bool(forKey: QueuePreferences.SBQueueRenameChapters)
+ self.addChapters = ud.bool(forKey: QueuePreferences.SBQueueAddChapters)
+ self.addChaptersInterval = ud.integer(forKey: QueuePreferences.SBQueueAddChaptersInterval)
+ self.addChaptersPolicy = ud.integer(forKey: QueuePreferences.SBQueueAddChaptersPolicy)
+ self.chaptersPreview = ud.bool(forKey: QueuePreferences.SBQueueChaptersPreview)
self.subtitles = ud.bool(forKey: QueuePreferences.SBQueueSubtitles)
if let presetName = ud.string(forKey: QueuePreferences.SBQueueSet) {
self.metadataSet = PresetManager.shared.item(name: presetName) as? MetadataPreset
@@ -148,6 +161,10 @@ final class QueuePreferences: NSObject {
QueuePreferences.SBQueueClearTrackName: false,
QueuePreferences.SBQueuePrettifyAudioTrackName: false,
QueuePreferences.SBQueueRenameChapters: false,
+ QueuePreferences.SBQueueAddChapters: false,
+ QueuePreferences.SBQueueAddChaptersInterval: 5,
+ QueuePreferences.SBQueueAddChaptersPolicy: 0,
+ QueuePreferences.SBQueueChaptersPreview: false,
QueuePreferences.SBQueueMetadata: false,
QueuePreferences.SBQueueSubtitles: true,
QueuePreferences.SBQueueApplyColorSpace: false,
@@ -188,6 +205,10 @@ final class QueuePreferences: NSObject {
ud.set(clearTrackName, forKey: QueuePreferences.SBQueueClearTrackName)
ud.set(prettifyAudioTrackName, forKey: QueuePreferences.SBQueuePrettifyAudioTrackName)
ud.set(renameChapters, forKey: QueuePreferences.SBQueueRenameChapters)
+ ud.set(addChapters, forKey: QueuePreferences.SBQueueAddChapters)
+ ud.set(addChaptersInterval, forKey: QueuePreferences.SBQueueAddChaptersInterval)
+ ud.set(addChaptersPolicy, forKey: QueuePreferences.SBQueueAddChaptersPolicy)
+ ud.set(chaptersPreview, forKey: QueuePreferences.SBQueueChaptersPreview)
ud.set(subtitles, forKey: QueuePreferences.SBQueueSubtitles)
ud.set(metadataSet?.title, forKey: QueuePreferences.SBQueueSet)
diff --git a/MP42Foundation b/MP42Foundation
index ab93941..c632539 160000
--- a/MP42Foundation
+++ b/MP42Foundation
@@ -1 +1 @@
-Subproject commit ab939411c392ac068df55178f7b2f932459374db
+Subproject commit c63253903d006904502e0e3aec0633ecb8f68484
diff --git a/NEWS.markdown b/NEWS.markdown
index 26e4240..6217df6 100644
--- a/NEWS.markdown
+++ b/NEWS.markdown
@@ -26,7 +26,7 @@
- Added additional keyboard shortcuts [Mock1]
- Added support for unofficial metadata often used for Audiobooks
- Make it possible to use the services ids in the metadata map
-- Fixed the "Insert a chaper at the beginning" menu item action
+- Fixed the "Insert a chapter at the beginning" menu item action
- Fixed Swedish Ratings
- Fixed the "None" artwork option in the Queue
@@ -172,7 +172,7 @@
## Subler 1.6.4
-* Fixes an issue that prevented uncheking tracks in the import sheet.
+* Fixes an issue that prevented unchecking tracks in the import sheet.
* Fixes an issue that prevented the parsing of some metadata.
* Improves Srt export. Subtitles tracks with edit lists are properly exported.
* Improved VobSub OCR subtitles timing.